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