1import 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")
177                line, col = map(int, pos.split("."))
178            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):
219    line, col = map(int, index.split(".")) # Fails on invalid index
220    return line, col
221