blob: d9361d0b1a478e777903a5837763ee8d65226c47 [file] [log] [blame]
Guido van Rossum504b0bf1999-01-02 21:28:54 +00001import string
2import re
3from Tkinter import *
4import tkMessageBox
5
6def get(root):
7 if not hasattr(root, "_searchengine"):
8 root._searchengine = SearchEngine(root)
9 # XXX This will never garbage-collect -- who cares
10 return root._searchengine
11
12class SearchEngine:
13
14 def __init__(self, root):
15 self.root = root
16 # State shared by search, replace, and grep;
17 # the search dialogs bind these to UI elements.
18 self.patvar = StringVar(root) # search pattern
19 self.revar = BooleanVar(root) # regular expression?
20 self.casevar = BooleanVar(root) # match case?
21 self.wordvar = BooleanVar(root) # match whole word?
22 self.wrapvar = BooleanVar(root) # wrap around buffer?
23 self.wrapvar.set(1) # (on by default)
24 self.backvar = BooleanVar(root) # search backwards?
25
26 # Access methods
27
28 def getpat(self):
29 return self.patvar.get()
30
31 def setpat(self, pat):
32 self.patvar.set(pat)
33
34 def isre(self):
35 return self.revar.get()
36
37 def iscase(self):
38 return self.casevar.get()
39
40 def isword(self):
41 return self.wordvar.get()
42
43 def iswrap(self):
44 return self.wrapvar.get()
45
46 def isback(self):
47 return self.backvar.get()
48
49 # Higher level access methods
50
51 def getcookedpat(self):
52 pat = self.getpat()
53 if not self.isre():
54 pat = re.escape(pat)
55 if self.isword():
56 pat = r"\b%s\b" % pat
57 return pat
58
59 def getprog(self):
60 pat = self.getpat()
61 if not pat:
62 self.report_error(pat, "Empty regular expression")
63 return None
64 pat = self.getcookedpat()
65 flags = 0
66 if not self.iscase():
67 flags = flags | re.IGNORECASE
68 try:
69 prog = re.compile(pat, flags)
70 except re.error, what:
71 try:
72 msg, col = what
73 except:
74 msg = str(what)
75 col = -1
76 self.report_error(pat, msg, col)
77 return None
78 return prog
79
80 def report_error(self, pat, msg, col=-1):
81 # Derived class could overrid this with something fancier
82 msg = "Error: " + str(msg)
83 if pat:
84 msg = msg + "\np\Pattern: " + str(pat)
85 if col >= 0:
86 msg = msg + "\nOffset: " + str(col)
87 tkMessageBox.showerror("Regular expression error",
88 msg, master=self.root)
89
90 def setcookedpat(self, pat):
91 if self.isre():
92 pat = re.escape(pat)
93 self.setpat(pat)
94
95 def search_text(self, text, prog=None, ok=0):
96 """Search a text widget for the pattern.
97
98 If prog is given, it should be the precompiled pattern.
99 Return a tuple (lineno, matchobj); None if not found.
100
101 This obeys the wrap and direction (back) settings.
102
103 The search starts at the selection (if there is one) or
104 at the insert mark (otherwise). If the search is forward,
105 it starts at the right of the selection; for a backward
106 search, it starts at the left end. An empty match exactly
107 at either end of the selection (or at the insert mark if
108 there is no selection) is ignored unless the ok flag is true
109 -- this is done to guarantee progress.
110
111 If the search is allowed to wrap around, it will return the
112 original selection if (and only if) it is the only match.
113
114 XXX When wrapping around and failing to find anything, the
115 portion of the text after the selection is searched twice :-(
116 """
117 if not prog:
118 prog = self.getprog()
119 if not prog:
120 return None # Compilation failed -- stop
121 wrap = self.wrapvar.get()
122 first, last = get_selection(text)
123 if self.isback():
124 if ok:
125 start = last
126 else:
127 start = first
128 line, col = get_line_col(start)
129 res = self.search_backward(text, prog, line, col, wrap, ok)
130 else:
131 if ok:
132 start = first
133 else:
134 start = last
135 line, col = get_line_col(start)
136 res = self.search_forward(text, prog, line, col, wrap, ok)
137 return res
138
139 def search_forward(self, text, prog, line, col, wrap, ok=0):
140 chars = text.get("%d.0" % line, "%d.0" % (line+1))
141 while chars:
142 m = prog.search(chars[:-1], col)
143 if m:
144 if ok or m.end() > col:
145 return line, m
146 line = line + 1
147 col = 0
148 ok = 1
149 chars = text.get("%d.0" % line, "%d.0" % (line+1))
150 if not chars and wrap:
151 wrap = 0
152 line = 1
153 chars = text.get("1.0", "2.0")
154 return None
155
156 def search_backward(self, text, prog, line, col, wrap, ok=0):
157 chars = text.get("%d.0" % line, "%d.0" % (line+1))
158 while 1:
159 m = search_reverse(prog, chars[:-1], col)
160 if m:
161 i, j = m.span()
162 if ok or m.start() < col:
163 return line, m
164 line = line - 1
165 ok = 1
166 if line <= 0:
167 if not wrap:
168 break
169 wrap = 0
170 pos = text.index("end-1c")
171 line, col = map(int, string.split(pos, "."))
172 chars = text.get("%d.0" % line, "%d.0" % (line+1))
173 col = len(chars) - 1
174 return None
175
176# Helper to search backwards in a string.
177# (Optimized for the case where the pattern isn't found.)
178
179def search_reverse(prog, chars, col):
180 m = prog.search(chars)
181 if not m:
182 return None
183 found = None
184 i, j = m.span()
185 while i < col and j <= col:
186 found = m
187 if i == j:
188 j = j+1
189 m = prog.search(chars, j)
190 if not m:
191 break
192 i, j = m.span()
193 return found
194
195# Helper to get selection end points, defaulting to insert mark.
196# Return a tuple of indices ("line.col" strings).
197
198def get_selection(text):
199 try:
200 first = text.index("sel.first")
201 last = text.index("sel.last")
202 except TclError:
203 first = last = None
204 if not first:
205 first = text.index("insert")
206 if not last:
207 last = first
208 return first, last
209
210# Helper to parse a text index into a (line, col) tuple.
211
212def get_line_col(index):
213 line, col = map(int, string.split(index, ".")) # Fails on invalid index
214 return line, col