jpayne@69
|
1 '''Define SearchEngine for search dialogs.'''
|
jpayne@69
|
2 import re
|
jpayne@69
|
3
|
jpayne@69
|
4 from tkinter import StringVar, BooleanVar, TclError
|
jpayne@69
|
5 import tkinter.messagebox as tkMessageBox
|
jpayne@69
|
6
|
jpayne@69
|
7 def get(root):
|
jpayne@69
|
8 '''Return the singleton SearchEngine instance for the process.
|
jpayne@69
|
9
|
jpayne@69
|
10 The single SearchEngine saves settings between dialog instances.
|
jpayne@69
|
11 If there is not a SearchEngine already, make one.
|
jpayne@69
|
12 '''
|
jpayne@69
|
13 if not hasattr(root, "_searchengine"):
|
jpayne@69
|
14 root._searchengine = SearchEngine(root)
|
jpayne@69
|
15 # This creates a cycle that persists until root is deleted.
|
jpayne@69
|
16 return root._searchengine
|
jpayne@69
|
17
|
jpayne@69
|
18
|
jpayne@69
|
19 class SearchEngine:
|
jpayne@69
|
20 """Handles searching a text widget for Find, Replace, and Grep."""
|
jpayne@69
|
21
|
jpayne@69
|
22 def __init__(self, root):
|
jpayne@69
|
23 '''Initialize Variables that save search state.
|
jpayne@69
|
24
|
jpayne@69
|
25 The dialogs bind these to the UI elements present in the dialogs.
|
jpayne@69
|
26 '''
|
jpayne@69
|
27 self.root = root # need for report_error()
|
jpayne@69
|
28 self.patvar = StringVar(root, '') # search pattern
|
jpayne@69
|
29 self.revar = BooleanVar(root, False) # regular expression?
|
jpayne@69
|
30 self.casevar = BooleanVar(root, False) # match case?
|
jpayne@69
|
31 self.wordvar = BooleanVar(root, False) # match whole word?
|
jpayne@69
|
32 self.wrapvar = BooleanVar(root, True) # wrap around buffer?
|
jpayne@69
|
33 self.backvar = BooleanVar(root, False) # search backwards?
|
jpayne@69
|
34
|
jpayne@69
|
35 # Access methods
|
jpayne@69
|
36
|
jpayne@69
|
37 def getpat(self):
|
jpayne@69
|
38 return self.patvar.get()
|
jpayne@69
|
39
|
jpayne@69
|
40 def setpat(self, pat):
|
jpayne@69
|
41 self.patvar.set(pat)
|
jpayne@69
|
42
|
jpayne@69
|
43 def isre(self):
|
jpayne@69
|
44 return self.revar.get()
|
jpayne@69
|
45
|
jpayne@69
|
46 def iscase(self):
|
jpayne@69
|
47 return self.casevar.get()
|
jpayne@69
|
48
|
jpayne@69
|
49 def isword(self):
|
jpayne@69
|
50 return self.wordvar.get()
|
jpayne@69
|
51
|
jpayne@69
|
52 def iswrap(self):
|
jpayne@69
|
53 return self.wrapvar.get()
|
jpayne@69
|
54
|
jpayne@69
|
55 def isback(self):
|
jpayne@69
|
56 return self.backvar.get()
|
jpayne@69
|
57
|
jpayne@69
|
58 # Higher level access methods
|
jpayne@69
|
59
|
jpayne@69
|
60 def setcookedpat(self, pat):
|
jpayne@69
|
61 "Set pattern after escaping if re."
|
jpayne@69
|
62 # called only in search.py: 66
|
jpayne@69
|
63 if self.isre():
|
jpayne@69
|
64 pat = re.escape(pat)
|
jpayne@69
|
65 self.setpat(pat)
|
jpayne@69
|
66
|
jpayne@69
|
67 def getcookedpat(self):
|
jpayne@69
|
68 pat = self.getpat()
|
jpayne@69
|
69 if not self.isre(): # if True, see setcookedpat
|
jpayne@69
|
70 pat = re.escape(pat)
|
jpayne@69
|
71 if self.isword():
|
jpayne@69
|
72 pat = r"\b%s\b" % pat
|
jpayne@69
|
73 return pat
|
jpayne@69
|
74
|
jpayne@69
|
75 def getprog(self):
|
jpayne@69
|
76 "Return compiled cooked search pattern."
|
jpayne@69
|
77 pat = self.getpat()
|
jpayne@69
|
78 if not pat:
|
jpayne@69
|
79 self.report_error(pat, "Empty regular expression")
|
jpayne@69
|
80 return None
|
jpayne@69
|
81 pat = self.getcookedpat()
|
jpayne@69
|
82 flags = 0
|
jpayne@69
|
83 if not self.iscase():
|
jpayne@69
|
84 flags = flags | re.IGNORECASE
|
jpayne@69
|
85 try:
|
jpayne@69
|
86 prog = re.compile(pat, flags)
|
jpayne@69
|
87 except re.error as what:
|
jpayne@69
|
88 args = what.args
|
jpayne@69
|
89 msg = args[0]
|
jpayne@69
|
90 col = args[1] if len(args) >= 2 else -1
|
jpayne@69
|
91 self.report_error(pat, msg, col)
|
jpayne@69
|
92 return None
|
jpayne@69
|
93 return prog
|
jpayne@69
|
94
|
jpayne@69
|
95 def report_error(self, pat, msg, col=-1):
|
jpayne@69
|
96 # Derived class could override this with something fancier
|
jpayne@69
|
97 msg = "Error: " + str(msg)
|
jpayne@69
|
98 if pat:
|
jpayne@69
|
99 msg = msg + "\nPattern: " + str(pat)
|
jpayne@69
|
100 if col >= 0:
|
jpayne@69
|
101 msg = msg + "\nOffset: " + str(col)
|
jpayne@69
|
102 tkMessageBox.showerror("Regular expression error",
|
jpayne@69
|
103 msg, master=self.root)
|
jpayne@69
|
104
|
jpayne@69
|
105 def search_text(self, text, prog=None, ok=0):
|
jpayne@69
|
106 '''Return (lineno, matchobj) or None for forward/backward search.
|
jpayne@69
|
107
|
jpayne@69
|
108 This function calls the right function with the right arguments.
|
jpayne@69
|
109 It directly return the result of that call.
|
jpayne@69
|
110
|
jpayne@69
|
111 Text is a text widget. Prog is a precompiled pattern.
|
jpayne@69
|
112 The ok parameter is a bit complicated as it has two effects.
|
jpayne@69
|
113
|
jpayne@69
|
114 If there is a selection, the search begin at either end,
|
jpayne@69
|
115 depending on the direction setting and ok, with ok meaning that
|
jpayne@69
|
116 the search starts with the selection. Otherwise, search begins
|
jpayne@69
|
117 at the insert mark.
|
jpayne@69
|
118
|
jpayne@69
|
119 To aid progress, the search functions do not return an empty
|
jpayne@69
|
120 match at the starting position unless ok is True.
|
jpayne@69
|
121 '''
|
jpayne@69
|
122
|
jpayne@69
|
123 if not prog:
|
jpayne@69
|
124 prog = self.getprog()
|
jpayne@69
|
125 if not prog:
|
jpayne@69
|
126 return None # Compilation failed -- stop
|
jpayne@69
|
127 wrap = self.wrapvar.get()
|
jpayne@69
|
128 first, last = get_selection(text)
|
jpayne@69
|
129 if self.isback():
|
jpayne@69
|
130 if ok:
|
jpayne@69
|
131 start = last
|
jpayne@69
|
132 else:
|
jpayne@69
|
133 start = first
|
jpayne@69
|
134 line, col = get_line_col(start)
|
jpayne@69
|
135 res = self.search_backward(text, prog, line, col, wrap, ok)
|
jpayne@69
|
136 else:
|
jpayne@69
|
137 if ok:
|
jpayne@69
|
138 start = first
|
jpayne@69
|
139 else:
|
jpayne@69
|
140 start = last
|
jpayne@69
|
141 line, col = get_line_col(start)
|
jpayne@69
|
142 res = self.search_forward(text, prog, line, col, wrap, ok)
|
jpayne@69
|
143 return res
|
jpayne@69
|
144
|
jpayne@69
|
145 def search_forward(self, text, prog, line, col, wrap, ok=0):
|
jpayne@69
|
146 wrapped = 0
|
jpayne@69
|
147 startline = line
|
jpayne@69
|
148 chars = text.get("%d.0" % line, "%d.0" % (line+1))
|
jpayne@69
|
149 while chars:
|
jpayne@69
|
150 m = prog.search(chars[:-1], col)
|
jpayne@69
|
151 if m:
|
jpayne@69
|
152 if ok or m.end() > col:
|
jpayne@69
|
153 return line, m
|
jpayne@69
|
154 line = line + 1
|
jpayne@69
|
155 if wrapped and line > startline:
|
jpayne@69
|
156 break
|
jpayne@69
|
157 col = 0
|
jpayne@69
|
158 ok = 1
|
jpayne@69
|
159 chars = text.get("%d.0" % line, "%d.0" % (line+1))
|
jpayne@69
|
160 if not chars and wrap:
|
jpayne@69
|
161 wrapped = 1
|
jpayne@69
|
162 wrap = 0
|
jpayne@69
|
163 line = 1
|
jpayne@69
|
164 chars = text.get("1.0", "2.0")
|
jpayne@69
|
165 return None
|
jpayne@69
|
166
|
jpayne@69
|
167 def search_backward(self, text, prog, line, col, wrap, ok=0):
|
jpayne@69
|
168 wrapped = 0
|
jpayne@69
|
169 startline = line
|
jpayne@69
|
170 chars = text.get("%d.0" % line, "%d.0" % (line+1))
|
jpayne@69
|
171 while 1:
|
jpayne@69
|
172 m = search_reverse(prog, chars[:-1], col)
|
jpayne@69
|
173 if m:
|
jpayne@69
|
174 if ok or m.start() < col:
|
jpayne@69
|
175 return line, m
|
jpayne@69
|
176 line = line - 1
|
jpayne@69
|
177 if wrapped and line < startline:
|
jpayne@69
|
178 break
|
jpayne@69
|
179 ok = 1
|
jpayne@69
|
180 if line <= 0:
|
jpayne@69
|
181 if not wrap:
|
jpayne@69
|
182 break
|
jpayne@69
|
183 wrapped = 1
|
jpayne@69
|
184 wrap = 0
|
jpayne@69
|
185 pos = text.index("end-1c")
|
jpayne@69
|
186 line, col = map(int, pos.split("."))
|
jpayne@69
|
187 chars = text.get("%d.0" % line, "%d.0" % (line+1))
|
jpayne@69
|
188 col = len(chars) - 1
|
jpayne@69
|
189 return None
|
jpayne@69
|
190
|
jpayne@69
|
191
|
jpayne@69
|
192 def search_reverse(prog, chars, col):
|
jpayne@69
|
193 '''Search backwards and return an re match object or None.
|
jpayne@69
|
194
|
jpayne@69
|
195 This is done by searching forwards until there is no match.
|
jpayne@69
|
196 Prog: compiled re object with a search method returning a match.
|
jpayne@69
|
197 Chars: line of text, without \\n.
|
jpayne@69
|
198 Col: stop index for the search; the limit for match.end().
|
jpayne@69
|
199 '''
|
jpayne@69
|
200 m = prog.search(chars)
|
jpayne@69
|
201 if not m:
|
jpayne@69
|
202 return None
|
jpayne@69
|
203 found = None
|
jpayne@69
|
204 i, j = m.span() # m.start(), m.end() == match slice indexes
|
jpayne@69
|
205 while i < col and j <= col:
|
jpayne@69
|
206 found = m
|
jpayne@69
|
207 if i == j:
|
jpayne@69
|
208 j = j+1
|
jpayne@69
|
209 m = prog.search(chars, j)
|
jpayne@69
|
210 if not m:
|
jpayne@69
|
211 break
|
jpayne@69
|
212 i, j = m.span()
|
jpayne@69
|
213 return found
|
jpayne@69
|
214
|
jpayne@69
|
215 def get_selection(text):
|
jpayne@69
|
216 '''Return tuple of 'line.col' indexes from selection or insert mark.
|
jpayne@69
|
217 '''
|
jpayne@69
|
218 try:
|
jpayne@69
|
219 first = text.index("sel.first")
|
jpayne@69
|
220 last = text.index("sel.last")
|
jpayne@69
|
221 except TclError:
|
jpayne@69
|
222 first = last = None
|
jpayne@69
|
223 if not first:
|
jpayne@69
|
224 first = text.index("insert")
|
jpayne@69
|
225 if not last:
|
jpayne@69
|
226 last = first
|
jpayne@69
|
227 return first, last
|
jpayne@69
|
228
|
jpayne@69
|
229 def get_line_col(index):
|
jpayne@69
|
230 '''Return (line, col) tuple of ints from 'line.col' string.'''
|
jpayne@69
|
231 line, col = map(int, index.split(".")) # Fails on invalid index
|
jpayne@69
|
232 return line, col
|
jpayne@69
|
233
|
jpayne@69
|
234
|
jpayne@69
|
235 if __name__ == "__main__":
|
jpayne@69
|
236 from unittest import main
|
jpayne@69
|
237 main('idlelib.idle_test.test_searchengine', verbosity=2)
|