1'''Define SearchEngine for search dialogs.'''
2import re
3from Tkinter import StringVar, BooleanVar, TclError
4import tkMessageBox
5
6def get(root):
7    '''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    '''
12    if not hasattr(root, "_searchengine"):
13        root._searchengine = SearchEngine(root)
14        # This creates a cycle that persists until root is deleted.
15    return root._searchengine
16
17class SearchEngine:
18    """Handles searching a text widget for Find, Replace, and Grep."""
19
20    def __init__(self, root):
21        '''Initialize Variables that save search state.
22
23        The dialogs bind these to the UI elements present in the dialogs.
24        '''
25        self.root = root  # need for report_error()
26        self.patvar = StringVar(root, '')   # search pattern
27        self.revar = BooleanVar(root, False)   # regular expression?
28        self.casevar = BooleanVar(root, False)   # match case?
29        self.wordvar = BooleanVar(root, False)   # match whole word?
30        self.wrapvar = BooleanVar(root, True)   # wrap around buffer?
31        self.backvar = BooleanVar(root, False)   # search backwards?
32
33    # Access methods
34
35    def getpat(self):
36        return self.patvar.get()
37
38    def setpat(self, pat):
39        self.patvar.set(pat)
40
41    def isre(self):
42        return self.revar.get()
43
44    def iscase(self):
45        return self.casevar.get()
46
47    def isword(self):
48        return self.wordvar.get()
49
50    def iswrap(self):
51        return self.wrapvar.get()
52
53    def isback(self):
54        return self.backvar.get()
55
56    # Higher level access methods
57
58    def setcookedpat(self, pat):
59        "Set pattern after escaping if re."
60        # called only in SearchDialog.py: 66
61        if self.isre():
62            pat = re.escape(pat)
63        self.setpat(pat)
64
65    def getcookedpat(self):
66        pat = self.getpat()
67        if not self.isre():  # if True, see setcookedpat
68            pat = re.escape(pat)
69        if self.isword():
70            pat = r"\b%s\b" % pat
71        return pat
72
73    def getprog(self):
74        "Return compiled cooked search pattern."
75        pat = self.getpat()
76        if not pat:
77            self.report_error(pat, "Empty regular expression")
78            return None
79        pat = self.getcookedpat()
80        flags = 0
81        if not self.iscase():
82            flags = flags | re.IGNORECASE
83        try:
84            prog = re.compile(pat, flags)
85        except re.error as what:
86            args = what.args
87            msg = args[0]
88            col = args[1] if len(args) >= 2 else -1
89            self.report_error(pat, msg, col)
90            return None
91        return prog
92
93    def report_error(self, pat, msg, col=-1):
94        # Derived class could override this with something fancier
95        msg = "Error: " + str(msg)
96        if pat:
97            msg = msg + "\nPattern: " + str(pat)
98        if col >= 0:
99            msg = msg + "\nOffset: " + str(col)
100        tkMessageBox.showerror("Regular expression error",
101                               msg, master=self.root)
102
103    def search_text(self, text, prog=None, ok=0):
104        '''Return (lineno, matchobj) or None for forward/backward search.
105
106        This function calls the right function with the right arguments.
107        It directly return the result of that call.
108
109        Text is a text widget. Prog is a precompiled pattern.
110        The ok parameter is a bit complicated as it has two effects.
111
112        If there is a selection, the search begin at either end,
113        depending on the direction setting and ok, with ok meaning that
114        the search starts with the selection. Otherwise, search begins
115        at the insert mark.
116
117        To aid progress, the search functions do not return an empty
118        match at the starting position unless ok is True.
119        '''
120
121        if not prog:
122            prog = self.getprog()
123            if not prog:
124                return None # Compilation failed -- stop
125        wrap = self.wrapvar.get()
126        first, last = get_selection(text)
127        if self.isback():
128            if ok:
129                start = last
130            else:
131                start = first
132            line, col = get_line_col(start)
133            res = self.search_backward(text, prog, line, col, wrap, ok)
134        else:
135            if ok:
136                start = first
137            else:
138                start = last
139            line, col = get_line_col(start)
140            res = self.search_forward(text, prog, line, col, wrap, ok)
141        return res
142
143    def search_forward(self, text, prog, line, col, wrap, ok=0):
144        wrapped = 0
145        startline = line
146        chars = text.get("%d.0" % line, "%d.0" % (line+1))
147        while chars:
148            m = prog.search(chars[:-1], col)
149            if m:
150                if ok or m.end() > col:
151                    return line, m
152            line = line + 1
153            if wrapped and line > startline:
154                break
155            col = 0
156            ok = 1
157            chars = text.get("%d.0" % line, "%d.0" % (line+1))
158            if not chars and wrap:
159                wrapped = 1
160                wrap = 0
161                line = 1
162                chars = text.get("1.0", "2.0")
163        return None
164
165    def search_backward(self, text, prog, line, col, wrap, ok=0):
166        wrapped = 0
167        startline = line
168        chars = text.get("%d.0" % line, "%d.0" % (line+1))
169        while 1:
170            m = search_reverse(prog, chars[:-1], col)
171            if m:
172                if ok or m.start() < col:
173                    return line, m
174            line = line - 1
175            if wrapped and line < startline:
176                break
177            ok = 1
178            if line <= 0:
179                if not wrap:
180                    break
181                wrapped = 1
182                wrap = 0
183                pos = text.index("end-1c")
184                line, col = map(int, pos.split("."))
185            chars = text.get("%d.0" % line, "%d.0" % (line+1))
186            col = len(chars) - 1
187        return None
188
189def search_reverse(prog, chars, col):
190    '''Search backwards and return an re match object or None.
191
192    This is done by searching forwards until there is no match.
193    Prog: compiled re object with a search method returning a match.
194    Chars: line of text, without \\n.
195    Col: stop index for the search; the limit for match.end().
196    '''
197    m = prog.search(chars)
198    if not m:
199        return None
200    found = None
201    i, j = m.span()  # m.start(), m.end() == match slice indexes
202    while i < col and j <= col:
203        found = m
204        if i == j:
205            j = j+1
206        m = prog.search(chars, j)
207        if not m:
208            break
209        i, j = m.span()
210    return found
211
212def get_selection(text):
213    '''Return tuple of 'line.col' indexes from selection or insert mark.
214    '''
215    try:
216        first = text.index("sel.first")
217        last = text.index("sel.last")
218    except TclError:
219        first = last = None
220    if not first:
221        first = text.index("insert")
222    if not last:
223        last = first
224    return first, last
225
226def get_line_col(index):
227    '''Return (line, col) tuple of ints from 'line.col' string.'''
228    line, col = map(int, index.split(".")) # Fails on invalid index
229    return line, col
230
231if __name__ == "__main__":
232    import unittest
233    unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
234