Coverage for searchengine.py: 97%
158 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1'''Define SearchEngine for search dialogs.'''
2import re
4from tkinter import StringVar, BooleanVar, TclError
5from tkinter import messagebox
7def get(root):
8 '''Return the singleton SearchEngine instance for the process.
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"): 1l
14 root._searchengine = SearchEngine(root) 1l
15 # This creates a cycle that persists until root is deleted.
16 return root._searchengine 1l
19class SearchEngine:
20 """Handles searching a text widget for Find, Replace, and Grep."""
22 def __init__(self, root):
23 '''Initialize Variables that save search state.
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?
35 # Access methods
37 def getpat(self):
38 return self.patvar.get() 1gdija
40 def setpat(self, pat):
41 self.patvar.set(pat) 1gdij
43 def isre(self):
44 return self.revar.get() 1gdij
46 def iscase(self):
47 return self.casevar.get() 1di
49 def isword(self):
50 return self.wordvar.get() 1gdi
52 def iswrap(self):
53 return self.wrapvar.get() 1i
55 def isback(self):
56 return self.backvar.get() 1ia
58 # Higher level access methods
60 def setcookedpat(self, pat):
61 "Set pattern after escaping if re."
62 # called only in search.py: 66
63 if self.isre(): 1j
64 pat = re.escape(pat) 1j
65 self.setpat(pat) 1j
67 def getcookedpat(self):
68 pat = self.getpat() 1gd
69 if not self.isre(): # if True, see setcookedpat 1gd
70 pat = re.escape(pat) 1gd
71 if self.isword(): 1gd
72 pat = r"\b%s\b" % pat 1g
73 return pat 1gd
75 def getprog(self):
76 "Return compiled cooked search pattern."
77 pat = self.getpat() 1da
78 if not pat: 1da
79 self.report_error(pat, "Empty regular expression") 1da
80 return None 1da
81 pat = self.getcookedpat() 1d
82 flags = 0 1d
83 if not self.iscase(): 1d
84 flags = flags | re.IGNORECASE 1d
85 try: 1d
86 prog = re.compile(pat, flags) 1d
87 except re.error as e: 1d
88 self.report_error(pat, e.msg, e.pos) 1d
89 return None 1d
90 return prog 1d
92 def report_error(self, pat, msg, col=None):
93 # Derived class could override this with something fancier
94 msg = "Error: " + str(msg) 1dka
95 if pat: 1dka
96 msg = msg + "\nPattern: " + str(pat) 1dk
97 if col is not None: 1dka
98 msg = msg + "\nOffset: " + str(col) 1dk
99 messagebox.showerror("Regular expression error", 1dka
100 msg, master=self.root)
102 def search_text(self, text, prog=None, ok=0):
103 '''Return (lineno, matchobj) or None for forward/backward search.
105 This function calls the right function with the right arguments.
106 It directly return the result of that call.
108 Text is a text widget. Prog is a precompiled pattern.
109 The ok parameter is a bit complicated as it has two effects.
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.
116 To aid progress, the search functions do not return an empty
117 match at the starting position unless ok is True.
118 '''
120 if not prog: 1a
121 prog = self.getprog() 1a
122 if not prog: 122 ↛ 124line 122 didn't jump to line 124, because the condition on line 122 was never false1a
123 return None # Compilation failed -- stop 1a
124 wrap = self.wrapvar.get() 1a
125 first, last = get_selection(text) 1a
126 if self.isback(): 1a
127 if ok: 1a
128 start = last 1a
129 else:
130 start = first 1a
131 line, col = get_line_col(start) 1a
132 res = self.search_backward(text, prog, line, col, wrap, ok) 1a
133 else:
134 if ok: 1a
135 start = first 1a
136 else:
137 start = last 1a
138 line, col = get_line_col(start) 1a
139 res = self.search_forward(text, prog, line, col, wrap, ok) 1a
140 return res 1a
142 def search_forward(self, text, prog, line, col, wrap, ok=0):
143 wrapped = 0 1e
144 startline = line 1e
145 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 1e
146 while chars: 1e
147 m = prog.search(chars[:-1], col) 1e
148 if m: 1e
149 if ok or m.end() > col: 149 ↛ 151line 149 didn't jump to line 151, because the condition on line 149 was never false1e
150 return line, m 1e
151 line = line + 1 1e
152 if wrapped and line > startline: 1e
153 break 1e
154 col = 0 1e
155 ok = 1 1e
156 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 1e
157 if not chars and wrap: 1e
158 wrapped = 1 1e
159 wrap = 0 1e
160 line = 1 1e
161 chars = text.get("1.0", "2.0") 1e
162 return None 1e
164 def search_backward(self, text, prog, line, col, wrap, ok=0):
165 wrapped = 0 1b
166 startline = line 1b
167 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 1b
168 while True: 1b
169 m = search_reverse(prog, chars[:-1], col) 1b
170 if m: 1b
171 if ok or m.start() < col: 171 ↛ 173line 171 didn't jump to line 173, because the condition on line 171 was never false1b
172 return line, m 1b
173 line = line - 1 1b
174 if wrapped and line < startline: 1b
175 break 1b
176 ok = 1 1b
177 if line <= 0: 1b
178 if not wrap: 1b
179 break 1b
180 wrapped = 1 1b
181 wrap = 0 1b
182 pos = text.index("end-1c") 1b
183 line, col = map(int, pos.split(".")) 1b
184 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 1b
185 col = len(chars) - 1 1b
186 return None 1b
189def search_reverse(prog, chars, col):
190 '''Search backwards and return an re match object or None.
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) 1bf
198 if not m: 1bf
199 return None 1b
200 found = None 1bf
201 i, j = m.span() # m.start(), m.end() == match slice indexes 1bf
202 while i < col and j <= col: 1bf
203 found = m 1bf
204 if i == j: 1bf
205 j = j+1 1b
206 m = prog.search(chars, j) 1bf
207 if not m: 1bf
208 break 1bf
209 i, j = m.span() 1bf
210 return found 1bf
212def get_selection(text):
213 '''Return tuple of 'line.col' indexes from selection or insert mark.
214 '''
215 try: 1ha
216 first = text.index("sel.first") 1ha
217 last = text.index("sel.last") 1ha
218 except TclError: 1ha
219 first = last = None 1ha
220 if not first: 1ha
221 first = text.index("insert") 1ha
222 if not last: 1ha
223 last = first 1ha
224 return first, last 1ha
226def 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 1ma
229 return line, col 1ma
232if __name__ == "__main__": 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true
233 from unittest import main
234 main('idlelib.idle_test.test_searchengine', verbosity=2)