summaryrefslogtreecommitdiff
path: root/Lib/idlelib/searchengine.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/idlelib/searchengine.py')
-rw-r--r--Lib/idlelib/searchengine.py236
1 files changed, 236 insertions, 0 deletions
diff --git a/Lib/idlelib/searchengine.py b/Lib/idlelib/searchengine.py
new file mode 100644
index 0000000000..253f1b0831
--- /dev/null
+++ b/Lib/idlelib/searchengine.py
@@ -0,0 +1,236 @@
+'''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 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 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 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 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)