123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- '''Define SearchEngine for search dialogs.'''
- import re
- from tkinter import StringVar, BooleanVar, TclError
- from tkinter import messagebox
- 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 search.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 e:
- self.report_error(pat, e.msg, e.pos)
- return None
- return prog
- def report_error(self, pat, msg, col=None):
- # Derived class could override this with something fancier
- msg = "Error: " + str(msg)
- if pat:
- msg = msg + "\nPattern: " + str(pat)
- if col is not None:
- msg = msg + "\nOffset: " + str(col)
- messagebox.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 parameter is 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 True:
- 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__":
- from unittest import main
- main('idlelib.idle_test.test_searchengine', verbosity=2)
|