summaryrefslogtreecommitdiffstats
path: root/Tools/idle/SearchEngine.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/idle/SearchEngine.py')
-rw-r--r--Tools/idle/SearchEngine.py214
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