1 '''Define SearchEngine for search dialogs.'''
2 import re
3
4 from tkinter import StringVar, BooleanVar, TclError
5 from tkinter import messagebox
6
7 def get(root):
8 '''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 '''
13 if not hasattr(root, "_searchengine"):
14 root._searchengine = SearchEngine(root)
15 # This creates a cycle that persists until root is deleted.
16 return root._searchengine
17
18
19 class ESC[4;38;5;81mSearchEngine:
20 """Handles searching a text widget for Find, Replace, and Grep."""
21
22 def __init__(self, root):
23 '''Initialize Variables that save search state.
24
25 The dialogs bind these to the UI elements present in the dialogs.
26 '''
27 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?
34
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
60 def setcookedpat(self, pat):
61 "Set pattern after escaping if re."
62 # called only in search.py: 66
63 if self.isre():
64 pat = re.escape(pat)
65 self.setpat(pat)
66
67 def getcookedpat(self):
68 pat = self.getpat()
69 if not self.isre(): # if True, see setcookedpat
70 pat = re.escape(pat)
71 if self.isword():
72 pat = r"\b%s\b" % pat
73 return pat
74
75 def getprog(self):
76 "Return compiled cooked search pattern."
77 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)
87 except re.error as e:
88 self.report_error(pat, e.msg, e.pos)
89 return None
90 return prog
91
92 def report_error(self, pat, msg, col=None):
93 # Derived class could override this with something fancier
94 msg = "Error: " + str(msg)
95 if pat:
96 msg = msg + "\nPattern: " + str(pat)
97 if col is not None:
98 msg = msg + "\nOffset: " + str(col)
99 messagebox.showerror("Regular expression error",
100 msg, master=self.root)
101
102 def search_text(self, text, prog=None, ok=0):
103 '''Return (lineno, matchobj) or None for forward/backward search.
104
105 This function calls the right function with the right arguments.
106 It directly return the result of that call.
107
108 Text is a text widget. Prog is a precompiled pattern.
109 The ok parameter is a bit complicated as it has two effects.
110
111 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.
118 '''
119
120 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 True:
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")
183 line, col = map(int, pos.split("."))
184 chars = text.get("%d.0" % line, "%d.0" % (line+1))
185 col = len(chars) - 1
186 return None
187
188
189 def search_reverse(prog, chars, col):
190 '''Search backwards and return an re match object or None.
191
192 This is done by searching forwards until there is no match.
193 Prog: compiled re object with a search method returning a match.
194 Chars: line of text, without \\n.
195 Col: stop index for the search; the limit for match.end().
196 '''
197 m = prog.search(chars)
198 if not m:
199 return None
200 found = None
201 i, j = m.span() # m.start(), m.end() == match slice indexes
202 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
212 def get_selection(text):
213 '''Return tuple of 'line.col' indexes from selection or insert mark.
214 '''
215 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
226 def get_line_col(index):
227 '''Return (line, col) tuple of ints from 'line.col' string.'''
228 line, col = map(int, index.split(".")) # Fails on invalid index
229 return line, col
230
231
232 if __name__ == "__main__":
233 from unittest import main
234 main('idlelib.idle_test.test_searchengine', verbosity=2)