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