| import re | 
 | from Tkinter import * | 
 | import tkMessageBox | 
 |  | 
 | def get(root): | 
 |     if not hasattr(root, "_searchengine"): | 
 |         root._searchengine = SearchEngine(root) | 
 |         # XXX This will never garbage-collect -- who cares | 
 |     return root._searchengine | 
 |  | 
 | class SearchEngine: | 
 |  | 
 |     def __init__(self, root): | 
 |         self.root = root | 
 |         # State shared by search, replace, and grep; | 
 |         # the search dialogs bind these to UI elements. | 
 |         self.patvar = StringVar(root)           # search pattern | 
 |         self.revar = BooleanVar(root)           # regular expression? | 
 |         self.casevar = BooleanVar(root)         # match case? | 
 |         self.wordvar = BooleanVar(root)         # match whole word? | 
 |         self.wrapvar = BooleanVar(root)         # wrap around buffer? | 
 |         self.wrapvar.set(1)                     # (on by default) | 
 |         self.backvar = BooleanVar(root)         # 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 getcookedpat(self): | 
 |         pat = self.getpat() | 
 |         if not self.isre(): | 
 |             pat = re.escape(pat) | 
 |         if self.isword(): | 
 |             pat = r"\b%s\b" % pat | 
 |         return pat | 
 |  | 
 |     def getprog(self): | 
 |         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, what: | 
 |             try: | 
 |                 msg, col = what | 
 |             except: | 
 |                 msg = str(what) | 
 |                 col = -1 | 
 |             self.report_error(pat, msg, col) | 
 |             return None | 
 |         return prog | 
 |  | 
 |     def report_error(self, pat, msg, col=-1): | 
 |         # Derived class could overrid this with something fancier | 
 |         msg = "Error: " + str(msg) | 
 |         if pat: | 
 |             msg = msg + "\np\Pattern: " + str(pat) | 
 |         if col >= 0: | 
 |             msg = msg + "\nOffset: " + str(col) | 
 |         tkMessageBox.showerror("Regular expression error", | 
 |                                msg, master=self.root) | 
 |  | 
 |     def setcookedpat(self, pat): | 
 |         if self.isre(): | 
 |             pat = re.escape(pat) | 
 |         self.setpat(pat) | 
 |  | 
 |     def search_text(self, text, prog=None, ok=0): | 
 |         """Search a text widget for the pattern. | 
 |  | 
 |         If prog is given, it should be the precompiled pattern. | 
 |         Return a tuple (lineno, matchobj); None if not found. | 
 |  | 
 |         This obeys the wrap and direction (back) settings. | 
 |  | 
 |         The search starts at the selection (if there is one) or | 
 |         at the insert mark (otherwise).  If the search is forward, | 
 |         it starts at the right of the selection; for a backward | 
 |         search, it starts at the left end.  An empty match exactly | 
 |         at either end of the selection (or at the insert mark if | 
 |         there is no selection) is ignored  unless the ok flag is true | 
 |         -- this is done to guarantee progress. | 
 |  | 
 |         If the search is allowed to wrap around, it will return the | 
 |         original selection if (and only if) it is the only match. | 
 |  | 
 |         """ | 
 |         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 | 
 |  | 
 | # Helper to search backwards in a string. | 
 | # (Optimized for the case where the pattern isn't found.) | 
 |  | 
 | def search_reverse(prog, chars, col): | 
 |     m = prog.search(chars) | 
 |     if not m: | 
 |         return None | 
 |     found = None | 
 |     i, j = m.span() | 
 |     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 | 
 |  | 
 | # Helper to get selection end points, defaulting to insert mark. | 
 | # Return a tuple of indices ("line.col" strings). | 
 |  | 
 | def get_selection(text): | 
 |     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 | 
 |  | 
 | # Helper to parse a text index into a (line, col) tuple. | 
 |  | 
 | def get_line_col(index): | 
 |     line, col = map(int, index.split(".")) # Fails on invalid index | 
 |     return line, col |