blob: ea6f1dede69b3c04b5fa6e499f047250387d39b1 [file] [log] [blame]
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -04001'''Define SearchEngine for search dialogs.'''
David Scherer7aced172000-08-15 01:13:23 +00002import re
Terry Jan Reedy4c427352013-08-31 16:27:08 -04003from Tkinter import StringVar, BooleanVar, TclError
Georg Brandl6634bf22008-05-20 07:13:37 +00004import tkMessageBox
David Scherer7aced172000-08-15 01:13:23 +00005
6def get(root):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -04007 '''Return the singleton SearchEngine instance for the process.
8
9 The single SearchEngine saves settings between dialog instances.
10 If there is not a SearchEngine already, make one.
11 '''
David Scherer7aced172000-08-15 01:13:23 +000012 if not hasattr(root, "_searchengine"):
13 root._searchengine = SearchEngine(root)
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040014 # This creates a cycle that persists until root is deleted.
David Scherer7aced172000-08-15 01:13:23 +000015 return root._searchengine
16
17class SearchEngine:
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040018 """Handles searching a text widget for Find, Replace, and Grep."""
David Scherer7aced172000-08-15 01:13:23 +000019
20 def __init__(self, root):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040021 '''Initialize Variables that save search state.
22
23 The dialogs bind these to the UI elements present in the dialogs.
24 '''
Terry Jan Reedy4c427352013-08-31 16:27:08 -040025 self.root = root # need for report_error()
26 self.patvar = StringVar(root, '') # search pattern
27 self.revar = BooleanVar(root, False) # regular expression?
28 self.casevar = BooleanVar(root, False) # match case?
29 self.wordvar = BooleanVar(root, False) # match whole word?
30 self.wrapvar = BooleanVar(root, True) # wrap around buffer?
31 self.backvar = BooleanVar(root, False) # search backwards?
David Scherer7aced172000-08-15 01:13:23 +000032
33 # Access methods
34
35 def getpat(self):
36 return self.patvar.get()
37
38 def setpat(self, pat):
39 self.patvar.set(pat)
40
41 def isre(self):
42 return self.revar.get()
43
44 def iscase(self):
45 return self.casevar.get()
46
47 def isword(self):
48 return self.wordvar.get()
49
50 def iswrap(self):
51 return self.wrapvar.get()
52
53 def isback(self):
54 return self.backvar.get()
55
56 # Higher level access methods
57
Terry Jan Reedy4c427352013-08-31 16:27:08 -040058 def setcookedpat(self, pat):
59 "Set pattern after escaping if re."
60 # called only in SearchDialog.py: 66
61 if self.isre():
62 pat = re.escape(pat)
63 self.setpat(pat)
64
David Scherer7aced172000-08-15 01:13:23 +000065 def getcookedpat(self):
66 pat = self.getpat()
Terry Jan Reedy4c427352013-08-31 16:27:08 -040067 if not self.isre(): # if True, see setcookedpat
David Scherer7aced172000-08-15 01:13:23 +000068 pat = re.escape(pat)
69 if self.isword():
70 pat = r"\b%s\b" % pat
71 return pat
72
73 def getprog(self):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040074 "Return compiled cooked search pattern."
David Scherer7aced172000-08-15 01:13:23 +000075 pat = self.getpat()
76 if not pat:
77 self.report_error(pat, "Empty regular expression")
78 return None
79 pat = self.getcookedpat()
80 flags = 0
81 if not self.iscase():
82 flags = flags | re.IGNORECASE
83 try:
84 prog = re.compile(pat, flags)
Terry Jan Reedy2b149862013-06-29 00:59:34 -040085 except re.error as what:
David Scherer7aced172000-08-15 01:13:23 +000086 try:
87 msg, col = what
88 except:
89 msg = str(what)
90 col = -1
91 self.report_error(pat, msg, col)
92 return None
93 return prog
94
95 def report_error(self, pat, msg, col=-1):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -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 Reedy4c427352013-08-31 16:27:08 -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 Reedy4c427352013-08-31 16:27:08 -0400106 '''Return (lineno, matchobj) or None for forward/backward search.
David Scherer7aced172000-08-15 01:13:23 +0000107
Terry Jan Reedy4c427352013-08-31 16:27:08 -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 Reedy4c427352013-08-31 16:27:08 -0400111 Text is a text widget. Prog is a precompiled pattern.
112 The ok parameteris a bit complicated as it has two effects.
David Scherer7aced172000-08-15 01:13:23 +0000113
Terry Jan Reedy4c427352013-08-31 16:27:08 -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 Reedy41fca3e2013-08-19 01:05:09 -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
David Scherer7aced172000-08-15 01:13:23 +0000191def search_reverse(prog, chars, col):
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400192 '''Search backwards and return an re match object or None.
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400193
194 This is done by searching forwards until there is no match.
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400195 Prog: compiled re object with a search method returning a match.
196 Chars: line of text, without \n.
197 Col: stop index for the search; the limit for match.end().
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400198 '''
David Scherer7aced172000-08-15 01:13:23 +0000199 m = prog.search(chars)
200 if not m:
201 return None
202 found = None
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400203 i, j = m.span() # m.start(), m.end() == match slice indexes
David Scherer7aced172000-08-15 01:13:23 +0000204 while i < col and j <= col:
205 found = m
206 if i == j:
207 j = j+1
208 m = prog.search(chars, j)
209 if not m:
210 break
211 i, j = m.span()
212 return found
213
David Scherer7aced172000-08-15 01:13:23 +0000214def get_selection(text):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400215 '''Return tuple of 'line.col' indexes from selection or insert mark.
216 '''
David Scherer7aced172000-08-15 01:13:23 +0000217 try:
218 first = text.index("sel.first")
219 last = text.index("sel.last")
220 except TclError:
221 first = last = None
222 if not first:
223 first = text.index("insert")
224 if not last:
225 last = first
226 return first, last
227
David Scherer7aced172000-08-15 01:13:23 +0000228def get_line_col(index):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400229 '''Return (line, col) tuple of ints from 'line.col' string.'''
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000230 line, col = map(int, index.split(".")) # Fails on invalid index
David Scherer7aced172000-08-15 01:13:23 +0000231 return line, col
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400232
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400233if __name__ == "__main__":
234 from test import support; support.use_resources = ['gui']
235 import unittest
236 unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)