blob: e37975104abcf349bec3bf1aa10b9b25f89c94ba [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +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 """
115 if not prog:
116 prog = self.getprog()
117 if not prog:
118 return None # Compilation failed -- stop
119 wrap = self.wrapvar.get()
120 first, last = get_selection(text)
121 if self.isback():
122 if ok:
123 start = last
124 else:
125 start = first
126 line, col = get_line_col(start)
127 res = self.search_backward(text, prog, line, col, wrap, ok)
128 else:
129 if ok:
130 start = first
131 else:
132 start = last
133 line, col = get_line_col(start)
134 res = self.search_forward(text, prog, line, col, wrap, ok)
135 return res
136
137 def search_forward(self, text, prog, line, col, wrap, ok=0):
138 wrapped = 0
139 startline = line
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 if wrapped and line > startline:
148 break
149 col = 0
150 ok = 1
151 chars = text.get("%d.0" % line, "%d.0" % (line+1))
152 if not chars and wrap:
153 wrapped = 1
154 wrap = 0
155 line = 1
156 chars = text.get("1.0", "2.0")
157 return None
158
159 def search_backward(self, text, prog, line, col, wrap, ok=0):
160 wrapped = 0
161 startline = line
162 chars = text.get("%d.0" % line, "%d.0" % (line+1))
163 while 1:
164 m = search_reverse(prog, chars[:-1], col)
165 if m:
166 if ok or m.start() < col:
167 return line, m
168 line = line - 1
169 if wrapped and line < startline:
170 break
171 ok = 1
172 if line <= 0:
173 if not wrap:
174 break
175 wrapped = 1
176 wrap = 0
177 pos = text.index("end-1c")
178 line, col = map(int, string.split(pos, "."))
179 chars = text.get("%d.0" % line, "%d.0" % (line+1))
180 col = len(chars) - 1
181 return None
182
183# Helper to search backwards in a string.
184# (Optimized for the case where the pattern isn't found.)
185
186def search_reverse(prog, chars, col):
187 m = prog.search(chars)
188 if not m:
189 return None
190 found = None
191 i, j = m.span()
192 while i < col and j <= col:
193 found = m
194 if i == j:
195 j = j+1
196 m = prog.search(chars, j)
197 if not m:
198 break
199 i, j = m.span()
200 return found
201
202# Helper to get selection end points, defaulting to insert mark.
203# Return a tuple of indices ("line.col" strings).
204
205def get_selection(text):
206 try:
207 first = text.index("sel.first")
208 last = text.index("sel.last")
209 except TclError:
210 first = last = None
211 if not first:
212 first = text.index("insert")
213 if not last:
214 last = first
215 return first, last
216
217# Helper to parse a text index into a (line, col) tuple.
218
219def get_line_col(index):
220 line, col = map(int, string.split(index, ".")) # Fails on invalid index
221 return line, col