|  | import re | 
|  | from tkinter import * | 
|  | import tkinter.messagebox as 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 as 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 |