blob: 963dfd39fe6bf3020011f990e1ba1b3593d19640 [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:
Terry Jan Reedy8119c132014-01-28 23:13:35 -050086 args = what.args
87 msg = args[0]
Terry Jan Reedy228b99e2014-07-01 21:33:26 -040088 col = args[1] if len(args) >= 2 else -1
David Scherer7aced172000-08-15 01:13:23 +000089 self.report_error(pat, msg, col)
90 return None
91 return prog
92
93 def report_error(self, pat, msg, col=-1):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040094 # Derived class could override this with something fancier
David Scherer7aced172000-08-15 01:13:23 +000095 msg = "Error: " + str(msg)
96 if pat:
Terry Jan Reedy4c427352013-08-31 16:27:08 -040097 msg = msg + "\nPattern: " + str(pat)
David Scherer7aced172000-08-15 01:13:23 +000098 if col >= 0:
99 msg = msg + "\nOffset: " + str(col)
100 tkMessageBox.showerror("Regular expression error",
101 msg, master=self.root)
102
David Scherer7aced172000-08-15 01:13:23 +0000103 def search_text(self, text, prog=None, ok=0):
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400104 '''Return (lineno, matchobj) or None for forward/backward search.
David Scherer7aced172000-08-15 01:13:23 +0000105
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400106 This function calls the right function with the right arguments.
107 It directly return the result of that call.
David Scherer7aced172000-08-15 01:13:23 +0000108
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400109 Text is a text widget. Prog is a precompiled pattern.
110 The ok parameteris a bit complicated as it has two effects.
David Scherer7aced172000-08-15 01:13:23 +0000111
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400112 If there is a selection, the search begin at either end,
113 depending on the direction setting and ok, with ok meaning that
114 the search starts with the selection. Otherwise, search begins
115 at the insert mark.
116
117 To aid progress, the search functions do not return an empty
118 match at the starting position unless ok is True.
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400119 '''
David Scherer7aced172000-08-15 01:13:23 +0000120
David Scherer7aced172000-08-15 01:13:23 +0000121 if not prog:
122 prog = self.getprog()
123 if not prog:
124 return None # Compilation failed -- stop
125 wrap = self.wrapvar.get()
126 first, last = get_selection(text)
127 if self.isback():
128 if ok:
129 start = last
130 else:
131 start = first
132 line, col = get_line_col(start)
133 res = self.search_backward(text, prog, line, col, wrap, ok)
134 else:
135 if ok:
136 start = first
137 else:
138 start = last
139 line, col = get_line_col(start)
140 res = self.search_forward(text, prog, line, col, wrap, ok)
141 return res
142
143 def search_forward(self, text, prog, line, col, wrap, ok=0):
144 wrapped = 0
145 startline = line
146 chars = text.get("%d.0" % line, "%d.0" % (line+1))
147 while chars:
148 m = prog.search(chars[:-1], col)
149 if m:
150 if ok or m.end() > col:
151 return line, m
152 line = line + 1
153 if wrapped and line > startline:
154 break
155 col = 0
156 ok = 1
157 chars = text.get("%d.0" % line, "%d.0" % (line+1))
158 if not chars and wrap:
159 wrapped = 1
160 wrap = 0
161 line = 1
162 chars = text.get("1.0", "2.0")
163 return None
164
165 def search_backward(self, text, prog, line, col, wrap, ok=0):
166 wrapped = 0
167 startline = line
168 chars = text.get("%d.0" % line, "%d.0" % (line+1))
169 while 1:
170 m = search_reverse(prog, chars[:-1], col)
171 if m:
172 if ok or m.start() < col:
173 return line, m
174 line = line - 1
175 if wrapped and line < startline:
176 break
177 ok = 1
178 if line <= 0:
179 if not wrap:
180 break
181 wrapped = 1
182 wrap = 0
183 pos = text.index("end-1c")
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000184 line, col = map(int, pos.split("."))
David Scherer7aced172000-08-15 01:13:23 +0000185 chars = text.get("%d.0" % line, "%d.0" % (line+1))
186 col = len(chars) - 1
187 return None
188
David Scherer7aced172000-08-15 01:13:23 +0000189def search_reverse(prog, chars, col):
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400190 '''Search backwards and return an re match object or None.
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400191
192 This is done by searching forwards until there is no match.
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400193 Prog: compiled re object with a search method returning a match.
Serhiy Storchakac8113282015-04-03 18:12:32 +0300194 Chars: line of text, without \\n.
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400195 Col: stop index for the search; the limit for match.end().
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400196 '''
David Scherer7aced172000-08-15 01:13:23 +0000197 m = prog.search(chars)
198 if not m:
199 return None
200 found = None
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400201 i, j = m.span() # m.start(), m.end() == match slice indexes
David Scherer7aced172000-08-15 01:13:23 +0000202 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
David Scherer7aced172000-08-15 01:13:23 +0000212def get_selection(text):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400213 '''Return tuple of 'line.col' indexes from selection or insert mark.
214 '''
David Scherer7aced172000-08-15 01:13:23 +0000215 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
David Scherer7aced172000-08-15 01:13:23 +0000226def get_line_col(index):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400227 '''Return (line, col) tuple of ints from 'line.col' string.'''
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000228 line, col = map(int, index.split(".")) # Fails on invalid index
David Scherer7aced172000-08-15 01:13:23 +0000229 return line, col
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400230
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400231if __name__ == "__main__":
Terry Jan Reedy4c427352013-08-31 16:27:08 -0400232 import unittest
233 unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)