| '''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) |