blob: eddef581ab40a752b287ee43b052d2e65731d12d [file] [log] [blame]
Terry Jan Reedyca64d252013-08-19 01:05:19 -04001'''Define SearchEngine for search dialogs.'''
David Scherer7aced172000-08-15 01:13:23 +00002import re
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04003
Terry Jan Reedy31e4d322013-08-31 16:27:16 -04004from tkinter import StringVar, BooleanVar, TclError
Terry Jan Reedy879986d2021-01-25 06:33:18 -05005from tkinter import messagebox
David Scherer7aced172000-08-15 01:13:23 +00006
7def get(root):
Terry Jan Reedyca64d252013-08-19 01:05:19 -04008 '''Return the singleton SearchEngine instance for the process.
9
10 The single SearchEngine saves settings between dialog instances.
11 If there is not a SearchEngine already, make one.
12 '''
David Scherer7aced172000-08-15 01:13:23 +000013 if not hasattr(root, "_searchengine"):
14 root._searchengine = SearchEngine(root)
Terry Jan Reedyca64d252013-08-19 01:05:19 -040015 # This creates a cycle that persists until root is deleted.
David Scherer7aced172000-08-15 01:13:23 +000016 return root._searchengine
17
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040018
David Scherer7aced172000-08-15 01:13:23 +000019class SearchEngine:
Terry Jan Reedyca64d252013-08-19 01:05:19 -040020 """Handles searching a text widget for Find, Replace, and Grep."""
David Scherer7aced172000-08-15 01:13:23 +000021
22 def __init__(self, root):
Terry Jan Reedyca64d252013-08-19 01:05:19 -040023 '''Initialize Variables that save search state.
24
25 The dialogs bind these to the UI elements present in the dialogs.
26 '''
Terry Jan Reedy31e4d322013-08-31 16:27:16 -040027 self.root = root # need for report_error()
28 self.patvar = StringVar(root, '') # search pattern
29 self.revar = BooleanVar(root, False) # regular expression?
30 self.casevar = BooleanVar(root, False) # match case?
31 self.wordvar = BooleanVar(root, False) # match whole word?
32 self.wrapvar = BooleanVar(root, True) # wrap around buffer?
33 self.backvar = BooleanVar(root, False) # search backwards?
David Scherer7aced172000-08-15 01:13:23 +000034
35 # Access methods
36
37 def getpat(self):
38 return self.patvar.get()
39
40 def setpat(self, pat):
41 self.patvar.set(pat)
42
43 def isre(self):
44 return self.revar.get()
45
46 def iscase(self):
47 return self.casevar.get()
48
49 def isword(self):
50 return self.wordvar.get()
51
52 def iswrap(self):
53 return self.wrapvar.get()
54
55 def isback(self):
56 return self.backvar.get()
57
58 # Higher level access methods
59
Terry Jan Reedy31e4d322013-08-31 16:27:16 -040060 def setcookedpat(self, pat):
61 "Set pattern after escaping if re."
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040062 # called only in search.py: 66
Terry Jan Reedy31e4d322013-08-31 16:27:16 -040063 if self.isre():
64 pat = re.escape(pat)
65 self.setpat(pat)
66
David Scherer7aced172000-08-15 01:13:23 +000067 def getcookedpat(self):
68 pat = self.getpat()
Terry Jan Reedy31e4d322013-08-31 16:27:16 -040069 if not self.isre(): # if True, see setcookedpat
David Scherer7aced172000-08-15 01:13:23 +000070 pat = re.escape(pat)
71 if self.isword():
72 pat = r"\b%s\b" % pat
73 return pat
74
75 def getprog(self):
Terry Jan Reedyca64d252013-08-19 01:05:19 -040076 "Return compiled cooked search pattern."
David Scherer7aced172000-08-15 01:13:23 +000077 pat = self.getpat()
78 if not pat:
79 self.report_error(pat, "Empty regular expression")
80 return None
81 pat = self.getcookedpat()
82 flags = 0
83 if not self.iscase():
84 flags = flags | re.IGNORECASE
85 try:
86 prog = re.compile(pat, flags)
Serhiy Storchaka453bc1d2020-11-22 07:06:51 +020087 except re.error as e:
88 self.report_error(pat, e.msg, e.pos)
David Scherer7aced172000-08-15 01:13:23 +000089 return None
90 return prog
91
Serhiy Storchaka453bc1d2020-11-22 07:06:51 +020092 def report_error(self, pat, msg, col=None):
Terry Jan Reedyca64d252013-08-19 01:05:19 -040093 # Derived class could override this with something fancier
David Scherer7aced172000-08-15 01:13:23 +000094 msg = "Error: " + str(msg)
95 if pat:
Terry Jan Reedy31e4d322013-08-31 16:27:16 -040096 msg = msg + "\nPattern: " + str(pat)
Serhiy Storchaka453bc1d2020-11-22 07:06:51 +020097 if col is not None:
David Scherer7aced172000-08-15 01:13:23 +000098 msg = msg + "\nOffset: " + str(col)
Terry Jan Reedy879986d2021-01-25 06:33:18 -050099 messagebox.showerror("Regular expression error",
David Scherer7aced172000-08-15 01:13:23 +0000100 msg, master=self.root)
101
David Scherer7aced172000-08-15 01:13:23 +0000102 def search_text(self, text, prog=None, ok=0):
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400103 '''Return (lineno, matchobj) or None for forward/backward search.
David Scherer7aced172000-08-15 01:13:23 +0000104
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400105 This function calls the right function with the right arguments.
106 It directly return the result of that call.
David Scherer7aced172000-08-15 01:13:23 +0000107
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400108 Text is a text widget. Prog is a precompiled pattern.
Berker Peksag4882cac2015-04-14 09:30:01 +0300109 The ok parameter is a bit complicated as it has two effects.
David Scherer7aced172000-08-15 01:13:23 +0000110
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400111 If there is a selection, the search begin at either end,
112 depending on the direction setting and ok, with ok meaning that
113 the search starts with the selection. Otherwise, search begins
114 at the insert mark.
115
116 To aid progress, the search functions do not return an empty
117 match at the starting position unless ok is True.
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400118 '''
David Scherer7aced172000-08-15 01:13:23 +0000119
David Scherer7aced172000-08-15 01:13:23 +0000120 if not prog:
121 prog = self.getprog()
122 if not prog:
123 return None # Compilation failed -- stop
124 wrap = self.wrapvar.get()
125 first, last = get_selection(text)
126 if self.isback():
127 if ok:
128 start = last
129 else:
130 start = first
131 line, col = get_line_col(start)
132 res = self.search_backward(text, prog, line, col, wrap, ok)
133 else:
134 if ok:
135 start = first
136 else:
137 start = last
138 line, col = get_line_col(start)
139 res = self.search_forward(text, prog, line, col, wrap, ok)
140 return res
141
142 def search_forward(self, text, prog, line, col, wrap, ok=0):
143 wrapped = 0
144 startline = line
145 chars = text.get("%d.0" % line, "%d.0" % (line+1))
146 while chars:
147 m = prog.search(chars[:-1], col)
148 if m:
149 if ok or m.end() > col:
150 return line, m
151 line = line + 1
152 if wrapped and line > startline:
153 break
154 col = 0
155 ok = 1
156 chars = text.get("%d.0" % line, "%d.0" % (line+1))
157 if not chars and wrap:
158 wrapped = 1
159 wrap = 0
160 line = 1
161 chars = text.get("1.0", "2.0")
162 return None
163
164 def search_backward(self, text, prog, line, col, wrap, ok=0):
165 wrapped = 0
166 startline = line
167 chars = text.get("%d.0" % line, "%d.0" % (line+1))
168 while 1:
169 m = search_reverse(prog, chars[:-1], col)
170 if m:
171 if ok or m.start() < col:
172 return line, m
173 line = line - 1
174 if wrapped and line < startline:
175 break
176 ok = 1
177 if line <= 0:
178 if not wrap:
179 break
180 wrapped = 1
181 wrap = 0
182 pos = text.index("end-1c")
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000183 line, col = map(int, pos.split("."))
David Scherer7aced172000-08-15 01:13:23 +0000184 chars = text.get("%d.0" % line, "%d.0" % (line+1))
185 col = len(chars) - 1
186 return None
187
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -0400188
David Scherer7aced172000-08-15 01:13:23 +0000189def search_reverse(prog, chars, col):
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400190 '''Search backwards and return an re match object or None.
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400191
192 This is done by searching forwards until there is no match.
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400193 Prog: compiled re object with a search method returning a match.
Serhiy Storchaka9f8a8912015-04-03 18:12:41 +0300194 Chars: line of text, without \\n.
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400195 Col: stop index for the search; the limit for match.end().
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400196 '''
David Scherer7aced172000-08-15 01:13:23 +0000197 m = prog.search(chars)
198 if not m:
199 return None
200 found = None
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400201 i, j = m.span() # m.start(), m.end() == match slice indexes
David Scherer7aced172000-08-15 01:13:23 +0000202 while i < col and j <= col:
203 found = m
204 if i == j:
205 j = j+1
206 m = prog.search(chars, j)
207 if not m:
208 break
209 i, j = m.span()
210 return found
211
David Scherer7aced172000-08-15 01:13:23 +0000212def get_selection(text):
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400213 '''Return tuple of 'line.col' indexes from selection or insert mark.
214 '''
David Scherer7aced172000-08-15 01:13:23 +0000215 try:
216 first = text.index("sel.first")
217 last = text.index("sel.last")
218 except TclError:
219 first = last = None
220 if not first:
221 first = text.index("insert")
222 if not last:
223 last = first
224 return first, last
225
David Scherer7aced172000-08-15 01:13:23 +0000226def get_line_col(index):
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400227 '''Return (line, col) tuple of ints from 'line.col' string.'''
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000228 line, col = map(int, index.split(".")) # Fails on invalid index
David Scherer7aced172000-08-15 01:13:23 +0000229 return line, col
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400230
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400231
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400232if __name__ == "__main__":
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400233 from unittest import main
234 main('idlelib.idle_test.test_searchengine', verbosity=2)