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