diff options
Diffstat (limited to 'Tools/idle/SearchEngine.py')
-rw-r--r-- | Tools/idle/SearchEngine.py | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/Tools/idle/SearchEngine.py b/Tools/idle/SearchEngine.py new file mode 100644 index 0000000..d9361d0 --- /dev/null +++ b/Tools/idle/SearchEngine.py @@ -0,0 +1,214 @@ +import string +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. + + XXX When wrapping around and failing to find anything, the + portion of the text after the selection is searched twice :-( + """ + 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): + 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 + col = 0 + ok = 1 + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + if not chars and wrap: + wrap = 0 + line = 1 + chars = text.get("1.0", "2.0") + return None + + def search_backward(self, text, prog, line, col, wrap, ok=0): + chars = text.get("%d.0" % line, "%d.0" % (line+1)) + while 1: + m = search_reverse(prog, chars[:-1], col) + if m: + i, j = m.span() + if ok or m.start() < col: + return line, m + line = line - 1 + ok = 1 + if line <= 0: + if not wrap: + break + wrap = 0 + pos = text.index("end-1c") + line, col = map(int, string.split(pos, ".")) + 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, string.split(index, ".")) # Fails on invalid index + return line, col |