| '''Define SearchEngine for search dialogs.''' | 
 | import re | 
 | from tkinter import StringVar, BooleanVar, TclError | 
 | import tkinter.messagebox as tkMessageBox | 
 |  | 
 | def get(root): | 
 |     '''Return the singleton SearchEngine instance for the process. | 
 |  | 
 |     The single SearchEngine saves settings between dialog instances. | 
 |     If there is not a SearchEngine already, make one. | 
 |     ''' | 
 |     if not hasattr(root, "_searchengine"): | 
 |         root._searchengine = SearchEngine(root) | 
 |         # This creates a cycle that persists until root is deleted. | 
 |     return root._searchengine | 
 |  | 
 | class SearchEngine: | 
 |     """Handles searching a text widget for Find, Replace, and Grep.""" | 
 |  | 
 |     def __init__(self, root): | 
 |         '''Initialize Variables that save search state. | 
 |  | 
 |         The dialogs bind these to the UI elements present in the dialogs. | 
 |         ''' | 
 |         self.root = root  # need for report_error() | 
 |         self.patvar = StringVar(root, '')   # search pattern | 
 |         self.revar = BooleanVar(root, False)   # regular expression? | 
 |         self.casevar = BooleanVar(root, False)   # match case? | 
 |         self.wordvar = BooleanVar(root, False)   # match whole word? | 
 |         self.wrapvar = BooleanVar(root, True)   # wrap around buffer? | 
 |         self.backvar = BooleanVar(root, False)   # search backwards? | 
 |  | 
 |     # Access methods | 
 |  | 
 |     def getpat(self): | 
 |         return self.patvar.get() | 
 |  | 
 |     def setpat(self, pat): | 
 |         self.patvar.set(pat) | 
 |  | 
 |     def isre(self): | 
 |         return self.revar.get() | 
 |  | 
 |     def iscase(self): | 
 |         return self.casevar.get() | 
 |  | 
 |     def isword(self): | 
 |         return self.wordvar.get() | 
 |  | 
 |     def iswrap(self): | 
 |         return self.wrapvar.get() | 
 |  | 
 |     def isback(self): | 
 |         return self.backvar.get() | 
 |  | 
 |     # Higher level access methods | 
 |  | 
 |     def setcookedpat(self, pat): | 
 |         "Set pattern after escaping if re." | 
 |         # called only in SearchDialog.py: 66 | 
 |         if self.isre(): | 
 |             pat = re.escape(pat) | 
 |         self.setpat(pat) | 
 |  | 
 |     def getcookedpat(self): | 
 |         pat = self.getpat() | 
 |         if not self.isre():  # if True, see setcookedpat | 
 |             pat = re.escape(pat) | 
 |         if self.isword(): | 
 |             pat = r"\b%s\b" % pat | 
 |         return pat | 
 |  | 
 |     def getprog(self): | 
 |         "Return compiled cooked search pattern." | 
 |         pat = self.getpat() | 
 |         if not pat: | 
 |             self.report_error(pat, "Empty regular expression") | 
 |             return None | 
 |         pat = self.getcookedpat() | 
 |         flags = 0 | 
 |         if not self.iscase(): | 
 |             flags = flags | re.IGNORECASE | 
 |         try: | 
 |             prog = re.compile(pat, flags) | 
 |         except re.error as what: | 
 |             args = what.args | 
 |             msg = args[0] | 
 |             col = args[1] if len(args) >= 2 else -1 | 
 |             self.report_error(pat, msg, col) | 
 |             return None | 
 |         return prog | 
 |  | 
 |     def report_error(self, pat, msg, col=-1): | 
 |         # Derived class could override this with something fancier | 
 |         msg = "Error: " + str(msg) | 
 |         if pat: | 
 |             msg = msg + "\nPattern: " + str(pat) | 
 |         if col >= 0: | 
 |             msg = msg + "\nOffset: " + str(col) | 
 |         tkMessageBox.showerror("Regular expression error", | 
 |                                msg, master=self.root) | 
 |  | 
 |     def search_text(self, text, prog=None, ok=0): | 
 |         '''Return (lineno, matchobj) or None for forward/backward search. | 
 |  | 
 |         This function calls the right function with the right arguments. | 
 |         It directly return the result of that call. | 
 |  | 
 |         Text is a text widget. Prog is a precompiled pattern. | 
 |         The ok parameteris a bit complicated as it has two effects. | 
 |  | 
 |         If there is a selection, the search begin at either end, | 
 |         depending on the direction setting and ok, with ok meaning that | 
 |         the search starts with the selection. Otherwise, search begins | 
 |         at the insert mark. | 
 |  | 
 |         To aid progress, the search functions do not return an empty | 
 |         match at the starting position unless ok is True. | 
 |         ''' | 
 |  | 
 |         if not prog: | 
 |             prog = self.getprog() | 
 |             if not prog: | 
 |                 return None # Compilation failed -- stop | 
 |         wrap = self.wrapvar.get() | 
 |         first, last = get_selection(text) | 
 |         if self.isback(): | 
 |             if ok: | 
 |                 start = last | 
 |             else: | 
 |                 start = first | 
 |             line, col = get_line_col(start) | 
 |             res = self.search_backward(text, prog, line, col, wrap, ok) | 
 |         else: | 
 |             if ok: | 
 |                 start = first | 
 |             else: | 
 |                 start = last | 
 |             line, col = get_line_col(start) | 
 |             res = self.search_forward(text, prog, line, col, wrap, ok) | 
 |         return res | 
 |  | 
 |     def search_forward(self, text, prog, line, col, wrap, ok=0): | 
 |         wrapped = 0 | 
 |         startline = line | 
 |         chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
 |         while chars: | 
 |             m = prog.search(chars[:-1], col) | 
 |             if m: | 
 |                 if ok or m.end() > col: | 
 |                     return line, m | 
 |             line = line + 1 | 
 |             if wrapped and line > startline: | 
 |                 break | 
 |             col = 0 | 
 |             ok = 1 | 
 |             chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
 |             if not chars and wrap: | 
 |                 wrapped = 1 | 
 |                 wrap = 0 | 
 |                 line = 1 | 
 |                 chars = text.get("1.0", "2.0") | 
 |         return None | 
 |  | 
 |     def search_backward(self, text, prog, line, col, wrap, ok=0): | 
 |         wrapped = 0 | 
 |         startline = line | 
 |         chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
 |         while 1: | 
 |             m = search_reverse(prog, chars[:-1], col) | 
 |             if m: | 
 |                 if ok or m.start() < col: | 
 |                     return line, m | 
 |             line = line - 1 | 
 |             if wrapped and line < startline: | 
 |                 break | 
 |             ok = 1 | 
 |             if line <= 0: | 
 |                 if not wrap: | 
 |                     break | 
 |                 wrapped = 1 | 
 |                 wrap = 0 | 
 |                 pos = text.index("end-1c") | 
 |                 line, col = map(int, pos.split(".")) | 
 |             chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
 |             col = len(chars) - 1 | 
 |         return None | 
 |  | 
 | def search_reverse(prog, chars, col): | 
 |     '''Search backwards and return an re match object or None. | 
 |  | 
 |     This is done by searching forwards until there is no match. | 
 |     Prog: compiled re object with a search method returning a match. | 
 |     Chars: line of text, without \\n. | 
 |     Col: stop index for the search; the limit for match.end(). | 
 |     ''' | 
 |     m = prog.search(chars) | 
 |     if not m: | 
 |         return None | 
 |     found = None | 
 |     i, j = m.span()  # m.start(), m.end() == match slice indexes | 
 |     while i < col and j <= col: | 
 |         found = m | 
 |         if i == j: | 
 |             j = j+1 | 
 |         m = prog.search(chars, j) | 
 |         if not m: | 
 |             break | 
 |         i, j = m.span() | 
 |     return found | 
 |  | 
 | def get_selection(text): | 
 |     '''Return tuple of 'line.col' indexes from selection or insert mark. | 
 |     ''' | 
 |     try: | 
 |         first = text.index("sel.first") | 
 |         last = text.index("sel.last") | 
 |     except TclError: | 
 |         first = last = None | 
 |     if not first: | 
 |         first = text.index("insert") | 
 |     if not last: | 
 |         last = first | 
 |     return first, last | 
 |  | 
 | def get_line_col(index): | 
 |     '''Return (line, col) tuple of ints from 'line.col' string.''' | 
 |     line, col = map(int, index.split(".")) # Fails on invalid index | 
 |     return line, col | 
 |  | 
 | if __name__ == "__main__": | 
 |     import unittest | 
 |     unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False) |