blob: 961a9218eee5ab59489e6d5c2b01416cbb498db7 [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
Georg Brandl6634bf22008-05-20 07:13:37 +00003from Tkinter import *
4import 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 '''
David Scherer7aced172000-08-15 01:13:23 +000025 self.root = root
David Scherer7aced172000-08-15 01:13:23 +000026 self.patvar = StringVar(root) # search pattern
27 self.revar = BooleanVar(root) # regular expression?
28 self.casevar = BooleanVar(root) # match case?
29 self.wordvar = BooleanVar(root) # match whole word?
30 self.wrapvar = BooleanVar(root) # wrap around buffer?
31 self.wrapvar.set(1) # (on by default)
32 self.backvar = BooleanVar(root) # search backwards?
33
34 # Access methods
35
36 def getpat(self):
37 return self.patvar.get()
38
39 def setpat(self, pat):
40 self.patvar.set(pat)
41
42 def isre(self):
43 return self.revar.get()
44
45 def iscase(self):
46 return self.casevar.get()
47
48 def isword(self):
49 return self.wordvar.get()
50
51 def iswrap(self):
52 return self.wrapvar.get()
53
54 def isback(self):
55 return self.backvar.get()
56
57 # Higher level access methods
58
59 def getcookedpat(self):
60 pat = self.getpat()
61 if not self.isre():
62 pat = re.escape(pat)
63 if self.isword():
64 pat = r"\b%s\b" % pat
65 return pat
66
67 def getprog(self):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040068 "Return compiled cooked search pattern."
David Scherer7aced172000-08-15 01:13:23 +000069 pat = self.getpat()
70 if not pat:
71 self.report_error(pat, "Empty regular expression")
72 return None
73 pat = self.getcookedpat()
74 flags = 0
75 if not self.iscase():
76 flags = flags | re.IGNORECASE
77 try:
78 prog = re.compile(pat, flags)
Terry Jan Reedy2b149862013-06-29 00:59:34 -040079 except re.error as what:
David Scherer7aced172000-08-15 01:13:23 +000080 try:
81 msg, col = what
82 except:
83 msg = str(what)
84 col = -1
85 self.report_error(pat, msg, col)
86 return None
87 return prog
88
89 def report_error(self, pat, msg, col=-1):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -040090 # Derived class could override this with something fancier
David Scherer7aced172000-08-15 01:13:23 +000091 msg = "Error: " + str(msg)
92 if pat:
93 msg = msg + "\np\Pattern: " + str(pat)
94 if col >= 0:
95 msg = msg + "\nOffset: " + str(col)
96 tkMessageBox.showerror("Regular expression error",
97 msg, master=self.root)
98
99 def setcookedpat(self, pat):
100 if self.isre():
101 pat = re.escape(pat)
102 self.setpat(pat)
103
104 def search_text(self, text, prog=None, ok=0):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400105 '''Return (lineno, matchobj) for prog in text widget, or None.
David Scherer7aced172000-08-15 01:13:23 +0000106
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400107 If prog is given, it should be a precompiled pattern.
108 Wrap (yes/no) and direction (forward/back) settings are used.
David Scherer7aced172000-08-15 01:13:23 +0000109
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400110 The search starts at the selection (if there is one) or at the
111 insert mark (otherwise). If the search is forward, it starts
112 at the right of the selection; for a backward search, it
113 starts at the left end. An empty match exactly at either end
114 of the selection (or at the insert mark if there is no
115 selection) is ignored unless the ok flag is true -- this is
116 done to guarantee progress.
David Scherer7aced172000-08-15 01:13:23 +0000117
118 If the search is allowed to wrap around, it will return the
119 original selection if (and only if) it is the only match.
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400120 '''
David Scherer7aced172000-08-15 01:13:23 +0000121
David Scherer7aced172000-08-15 01:13:23 +0000122 if not prog:
123 prog = self.getprog()
124 if not prog:
125 return None # Compilation failed -- stop
126 wrap = self.wrapvar.get()
127 first, last = get_selection(text)
128 if self.isback():
129 if ok:
130 start = last
131 else:
132 start = first
133 line, col = get_line_col(start)
134 res = self.search_backward(text, prog, line, col, wrap, ok)
135 else:
136 if ok:
137 start = first
138 else:
139 start = last
140 line, col = get_line_col(start)
141 res = self.search_forward(text, prog, line, col, wrap, ok)
142 return res
143
144 def search_forward(self, text, prog, line, col, wrap, ok=0):
145 wrapped = 0
146 startline = line
147 chars = text.get("%d.0" % line, "%d.0" % (line+1))
148 while chars:
149 m = prog.search(chars[:-1], col)
150 if m:
151 if ok or m.end() > col:
152 return line, m
153 line = line + 1
154 if wrapped and line > startline:
155 break
156 col = 0
157 ok = 1
158 chars = text.get("%d.0" % line, "%d.0" % (line+1))
159 if not chars and wrap:
160 wrapped = 1
161 wrap = 0
162 line = 1
163 chars = text.get("1.0", "2.0")
164 return None
165
166 def search_backward(self, text, prog, line, col, wrap, ok=0):
167 wrapped = 0
168 startline = line
169 chars = text.get("%d.0" % line, "%d.0" % (line+1))
170 while 1:
171 m = search_reverse(prog, chars[:-1], col)
172 if m:
173 if ok or m.start() < col:
174 return line, m
175 line = line - 1
176 if wrapped and line < startline:
177 break
178 ok = 1
179 if line <= 0:
180 if not wrap:
181 break
182 wrapped = 1
183 wrap = 0
184 pos = text.index("end-1c")
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000185 line, col = map(int, pos.split("."))
David Scherer7aced172000-08-15 01:13:23 +0000186 chars = text.get("%d.0" % line, "%d.0" % (line+1))
187 col = len(chars) - 1
188 return None
189
David Scherer7aced172000-08-15 01:13:23 +0000190def search_reverse(prog, chars, col):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400191 '''Search backwards in a string (line of text).
192
193 This is done by searching forwards until there is no match.
194 '''
David Scherer7aced172000-08-15 01:13:23 +0000195 m = prog.search(chars)
196 if not m:
197 return None
198 found = None
199 i, j = m.span()
200 while i < col and j <= col:
201 found = m
202 if i == j:
203 j = j+1
204 m = prog.search(chars, j)
205 if not m:
206 break
207 i, j = m.span()
208 return found
209
David Scherer7aced172000-08-15 01:13:23 +0000210def get_selection(text):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400211 '''Return tuple of 'line.col' indexes from selection or insert mark.
212 '''
David Scherer7aced172000-08-15 01:13:23 +0000213 try:
214 first = text.index("sel.first")
215 last = text.index("sel.last")
216 except TclError:
217 first = last = None
218 if not first:
219 first = text.index("insert")
220 if not last:
221 last = first
222 return first, last
223
David Scherer7aced172000-08-15 01:13:23 +0000224def get_line_col(index):
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400225 '''Return (line, col) tuple of ints from 'line.col' string.'''
Kurt B. Kaiserd1ec9402002-09-18 03:14:11 +0000226 line, col = map(int, index.split(".")) # Fails on invalid index
David Scherer7aced172000-08-15 01:13:23 +0000227 return line, col
Terry Jan Reedy41fca3e2013-08-19 01:05:09 -0400228
229##if __name__ == "__main__":
230## from test import support; support.use_resources = ['gui']
231## import unittest
232## unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)