• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'''Define SearchEngine for search dialogs.'''
2import re
3
4from tkinter import StringVar, BooleanVar, TclError
5from tkinter import messagebox
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 e:
88            self.report_error(pat, e.msg, e.pos)
89            return None
90        return prog
91
92    def report_error(self, pat, msg, col=None):
93        # Derived class could override this with something fancier
94        msg = "Error: " + str(msg)
95        if pat:
96            msg = msg + "\nPattern: " + str(pat)
97        if col is not None:
98            msg = msg + "\nOffset: " + str(col)
99        messagebox.showerror("Regular expression error",
100                               msg, master=self.root)
101
102    def search_text(self, text, prog=None, ok=0):
103        '''Return (lineno, matchobj) or None for forward/backward search.
104
105        This function calls the right function with the right arguments.
106        It directly return the result of that call.
107
108        Text is a text widget. Prog is a precompiled pattern.
109        The ok parameter is a bit complicated as it has two effects.
110
111        If there is a selection, the search begin at either end,
112        depending on the direction setting and ok, with ok meaning that
113        the search starts with the selection. Otherwise, search begins
114        at the insert mark.
115
116        To aid progress, the search functions do not return an empty
117        match at the starting position unless ok is True.
118        '''
119
120        if not prog:
121            prog = self.getprog()
122            if not prog:
123                return None # Compilation failed -- stop
124        wrap = self.wrapvar.get()
125        first, last = get_selection(text)
126        if self.isback():
127            if ok:
128                start = last
129            else:
130                start = first
131            line, col = get_line_col(start)
132            res = self.search_backward(text, prog, line, col, wrap, ok)
133        else:
134            if ok:
135                start = first
136            else:
137                start = last
138            line, col = get_line_col(start)
139            res = self.search_forward(text, prog, line, col, wrap, ok)
140        return res
141
142    def search_forward(self, text, prog, line, col, wrap, ok=0):
143        wrapped = 0
144        startline = line
145        chars = text.get("%d.0" % line, "%d.0" % (line+1))
146        while chars:
147            m = prog.search(chars[:-1], col)
148            if m:
149                if ok or m.end() > col:
150                    return line, m
151            line = line + 1
152            if wrapped and line > startline:
153                break
154            col = 0
155            ok = 1
156            chars = text.get("%d.0" % line, "%d.0" % (line+1))
157            if not chars and wrap:
158                wrapped = 1
159                wrap = 0
160                line = 1
161                chars = text.get("1.0", "2.0")
162        return None
163
164    def search_backward(self, text, prog, line, col, wrap, ok=0):
165        wrapped = 0
166        startline = line
167        chars = text.get("%d.0" % line, "%d.0" % (line+1))
168        while 1:
169            m = search_reverse(prog, chars[:-1], col)
170            if m:
171                if ok or m.start() < col:
172                    return line, m
173            line = line - 1
174            if wrapped and line < startline:
175                break
176            ok = 1
177            if line <= 0:
178                if not wrap:
179                    break
180                wrapped = 1
181                wrap = 0
182                pos = text.index("end-1c")
183                line, col = map(int, pos.split("."))
184            chars = text.get("%d.0" % line, "%d.0" % (line+1))
185            col = len(chars) - 1
186        return None
187
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
231
232if __name__ == "__main__":
233    from unittest import main
234    main('idlelib.idle_test.test_searchengine', verbosity=2)
235