blob: 911e7d4691cac11f45cf7f2fd86d641f147ce90c [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
Georg Brandl14fc4272008-05-17 18:39:55 +00005import tkinter.messagebox as tkMessageBox
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)
Guido van Rossumb940e112007-01-10 16:19:56 +000087 except re.error as what:
Terry Jan Reedy1e402952014-01-28 23:13:45 -050088 args = what.args
89 msg = args[0]
Terry Jan Reedy092b3cf2014-07-01 21:33:31 -040090 col = args[1] if len(args) >= 2 else -1
David Scherer7aced172000-08-15 01:13:23 +000091 self.report_error(pat, msg, col)
92 return None
93 return prog
94
95 def report_error(self, pat, msg, col=-1):
Terry Jan Reedyca64d252013-08-19 01:05:19 -040096 # Derived class could override this with something fancier
David Scherer7aced172000-08-15 01:13:23 +000097 msg = "Error: " + str(msg)
98 if pat:
Terry Jan Reedy31e4d322013-08-31 16:27:16 -040099 msg = msg + "\nPattern: " + str(pat)
David Scherer7aced172000-08-15 01:13:23 +0000100 if col >= 0:
101 msg = msg + "\nOffset: " + str(col)
102 tkMessageBox.showerror("Regular expression error",
103 msg, master=self.root)
104
David Scherer7aced172000-08-15 01:13:23 +0000105 def search_text(self, text, prog=None, ok=0):
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400106 '''Return (lineno, matchobj) or None for forward/backward search.
David Scherer7aced172000-08-15 01:13:23 +0000107
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400108 This function calls the right function with the right arguments.
109 It directly return the result of that call.
David Scherer7aced172000-08-15 01:13:23 +0000110
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400111 Text is a text widget. Prog is a precompiled pattern.
Berker Peksag4882cac2015-04-14 09:30:01 +0300112 The ok parameter is a bit complicated as it has two effects.
David Scherer7aced172000-08-15 01:13:23 +0000113
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400114 If there is a selection, the search begin at either end,
115 depending on the direction setting and ok, with ok meaning that
116 the search starts with the selection. Otherwise, search begins
117 at the insert mark.
118
119 To aid progress, the search functions do not return an empty
120 match at the starting position unless ok is True.
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400121 '''
David Scherer7aced172000-08-15 01:13:23 +0000122
David Scherer7aced172000-08-15 01:13:23 +0000123 if not prog:
124 prog = self.getprog()
125 if not prog:
126 return None # Compilation failed -- stop
127 wrap = self.wrapvar.get()
128 first, last = get_selection(text)
129 if self.isback():
130 if ok:
131 start = last
132 else:
133 start = first
134 line, col = get_line_col(start)
135 res = self.search_backward(text, prog, line, col, wrap, ok)
136 else:
137 if ok:
138 start = first
139 else:
140 start = last
141 line, col = get_line_col(start)
142 res = self.search_forward(text, prog, line, col, wrap, ok)
143 return res
144
145 def search_forward(self, text, prog, line, col, wrap, ok=0):
146 wrapped = 0
147 startline = line
148 chars = text.get("%d.0" % line, "%d.0" % (line+1))
149 while chars:
150 m = prog.search(chars[:-1], col)
151 if m:
152 if ok or m.end() > col:
153 return line, m
154 line = line + 1
155 if wrapped and line > startline:
156 break
157 col = 0
158 ok = 1
159 chars = text.get("%d.0" % line, "%d.0" % (line+1))
160 if not chars and wrap:
161 wrapped = 1
162 wrap = 0
163 line = 1
164 chars = text.get("1.0", "2.0")
165 return None
166
167 def search_backward(self, text, prog, line, col, wrap, ok=0):
168 wrapped = 0
169 startline = line
170 chars = text.get("%d.0" % line, "%d.0" % (line+1))
171 while 1:
172 m = search_reverse(prog, chars[:-1], col)
173 if m:
174 if ok or m.start() < col:
175 return line, m
176 line = line - 1
177 if wrapped and line < startline:
178 break
179 ok = 1
180 if line <= 0:
181 if not wrap:
182 break
183 wrapped = 1
184 wrap = 0
185 pos = text.index("end-1c")
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000186 line, col = map(int, pos.split("."))
David Scherer7aced172000-08-15 01:13:23 +0000187 chars = text.get("%d.0" % line, "%d.0" % (line+1))
188 col = len(chars) - 1
189 return None
190
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -0400191
David Scherer7aced172000-08-15 01:13:23 +0000192def search_reverse(prog, chars, col):
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400193 '''Search backwards and return an re match object or None.
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400194
195 This is done by searching forwards until there is no match.
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400196 Prog: compiled re object with a search method returning a match.
Serhiy Storchaka9f8a8912015-04-03 18:12:41 +0300197 Chars: line of text, without \\n.
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400198 Col: stop index for the search; the limit for match.end().
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400199 '''
David Scherer7aced172000-08-15 01:13:23 +0000200 m = prog.search(chars)
201 if not m:
202 return None
203 found = None
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400204 i, j = m.span() # m.start(), m.end() == match slice indexes
David Scherer7aced172000-08-15 01:13:23 +0000205 while i < col and j <= col:
206 found = m
207 if i == j:
208 j = j+1
209 m = prog.search(chars, j)
210 if not m:
211 break
212 i, j = m.span()
213 return found
214
David Scherer7aced172000-08-15 01:13:23 +0000215def get_selection(text):
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400216 '''Return tuple of 'line.col' indexes from selection or insert mark.
217 '''
David Scherer7aced172000-08-15 01:13:23 +0000218 try:
219 first = text.index("sel.first")
220 last = text.index("sel.last")
221 except TclError:
222 first = last = None
223 if not first:
224 first = text.index("insert")
225 if not last:
226 last = first
227 return first, last
228
David Scherer7aced172000-08-15 01:13:23 +0000229def get_line_col(index):
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400230 '''Return (line, col) tuple of ints from 'line.col' string.'''
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000231 line, col = map(int, index.split(".")) # Fails on invalid index
David Scherer7aced172000-08-15 01:13:23 +0000232 return line, col
Terry Jan Reedyca64d252013-08-19 01:05:19 -0400233
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400234
Terry Jan Reedy31e4d322013-08-31 16:27:16 -0400235if __name__ == "__main__":
Terry Jan Reedy4d921582018-06-19 19:12:52 -0400236 from unittest import main
237 main('idlelib.idle_test.test_searchengine', verbosity=2)