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