1'''Define SearchEngine for search dialogs.''' 2import re 3from Tkinter import StringVar, BooleanVar, TclError 4import tkMessageBox 5 6def get(root): 7 '''Return the singleton SearchEngine instance for the process. 8 9 The single SearchEngine saves settings between dialog instances. 10 If there is not a SearchEngine already, make one. 11 ''' 12 if not hasattr(root, "_searchengine"): 13 root._searchengine = SearchEngine(root) 14 # This creates a cycle that persists until root is deleted. 15 return root._searchengine 16 17class SearchEngine: 18 """Handles searching a text widget for Find, Replace, and Grep.""" 19 20 def __init__(self, root): 21 '''Initialize Variables that save search state. 22 23 The dialogs bind these to the UI elements present in the dialogs. 24 ''' 25 self.root = root # need for report_error() 26 self.patvar = StringVar(root, '') # search pattern 27 self.revar = BooleanVar(root, False) # regular expression? 28 self.casevar = BooleanVar(root, False) # match case? 29 self.wordvar = BooleanVar(root, False) # match whole word? 30 self.wrapvar = BooleanVar(root, True) # wrap around buffer? 31 self.backvar = BooleanVar(root, False) # search backwards? 32 33 # Access methods 34 35 def getpat(self): 36 return self.patvar.get() 37 38 def setpat(self, pat): 39 self.patvar.set(pat) 40 41 def isre(self): 42 return self.revar.get() 43 44 def iscase(self): 45 return self.casevar.get() 46 47 def isword(self): 48 return self.wordvar.get() 49 50 def iswrap(self): 51 return self.wrapvar.get() 52 53 def isback(self): 54 return self.backvar.get() 55 56 # Higher level access methods 57 58 def setcookedpat(self, pat): 59 "Set pattern after escaping if re." 60 # called only in SearchDialog.py: 66 61 if self.isre(): 62 pat = re.escape(pat) 63 self.setpat(pat) 64 65 def getcookedpat(self): 66 pat = self.getpat() 67 if not self.isre(): # if True, see setcookedpat 68 pat = re.escape(pat) 69 if self.isword(): 70 pat = r"\b%s\b" % pat 71 return pat 72 73 def getprog(self): 74 "Return compiled cooked search pattern." 75 pat = self.getpat() 76 if not pat: 77 self.report_error(pat, "Empty regular expression") 78 return None 79 pat = self.getcookedpat() 80 flags = 0 81 if not self.iscase(): 82 flags = flags | re.IGNORECASE 83 try: 84 prog = re.compile(pat, flags) 85 except re.error as what: 86 args = what.args 87 msg = args[0] 88 col = args[1] if len(args) >= 2 else -1 89 self.report_error(pat, msg, col) 90 return None 91 return prog 92 93 def report_error(self, pat, msg, col=-1): 94 # Derived class could override this with something fancier 95 msg = "Error: " + str(msg) 96 if pat: 97 msg = msg + "\nPattern: " + str(pat) 98 if col >= 0: 99 msg = msg + "\nOffset: " + str(col) 100 tkMessageBox.showerror("Regular expression error", 101 msg, master=self.root) 102 103 def search_text(self, text, prog=None, ok=0): 104 '''Return (lineno, matchobj) or None for forward/backward search. 105 106 This function calls the right function with the right arguments. 107 It directly return the result of that call. 108 109 Text is a text widget. Prog is a precompiled pattern. 110 The ok parameter is a bit complicated as it has two effects. 111 112 If there is a selection, the search begin at either end, 113 depending on the direction setting and ok, with ok meaning that 114 the search starts with the selection. Otherwise, search begins 115 at the insert mark. 116 117 To aid progress, the search functions do not return an empty 118 match at the starting position unless ok is True. 119 ''' 120 121 if not prog: 122 prog = self.getprog() 123 if not prog: 124 return None # Compilation failed -- stop 125 wrap = self.wrapvar.get() 126 first, last = get_selection(text) 127 if self.isback(): 128 if ok: 129 start = last 130 else: 131 start = first 132 line, col = get_line_col(start) 133 res = self.search_backward(text, prog, line, col, wrap, ok) 134 else: 135 if ok: 136 start = first 137 else: 138 start = last 139 line, col = get_line_col(start) 140 res = self.search_forward(text, prog, line, col, wrap, ok) 141 return res 142 143 def search_forward(self, text, prog, line, col, wrap, ok=0): 144 wrapped = 0 145 startline = line 146 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 147 while chars: 148 m = prog.search(chars[:-1], col) 149 if m: 150 if ok or m.end() > col: 151 return line, m 152 line = line + 1 153 if wrapped and line > startline: 154 break 155 col = 0 156 ok = 1 157 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 158 if not chars and wrap: 159 wrapped = 1 160 wrap = 0 161 line = 1 162 chars = text.get("1.0", "2.0") 163 return None 164 165 def search_backward(self, text, prog, line, col, wrap, ok=0): 166 wrapped = 0 167 startline = line 168 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 169 while 1: 170 m = search_reverse(prog, chars[:-1], col) 171 if m: 172 if ok or m.start() < col: 173 return line, m 174 line = line - 1 175 if wrapped and line < startline: 176 break 177 ok = 1 178 if line <= 0: 179 if not wrap: 180 break 181 wrapped = 1 182 wrap = 0 183 pos = text.index("end-1c") 184 line, col = map(int, pos.split(".")) 185 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 186 col = len(chars) - 1 187 return None 188 189def search_reverse(prog, chars, col): 190 '''Search backwards and return an re match object or None. 191 192 This is done by searching forwards until there is no match. 193 Prog: compiled re object with a search method returning a match. 194 Chars: line of text, without \\n. 195 Col: stop index for the search; the limit for match.end(). 196 ''' 197 m = prog.search(chars) 198 if not m: 199 return None 200 found = None 201 i, j = m.span() # m.start(), m.end() == match slice indexes 202 while i < col and j <= col: 203 found = m 204 if i == j: 205 j = j+1 206 m = prog.search(chars, j) 207 if not m: 208 break 209 i, j = m.span() 210 return found 211 212def get_selection(text): 213 '''Return tuple of 'line.col' indexes from selection or insert mark. 214 ''' 215 try: 216 first = text.index("sel.first") 217 last = text.index("sel.last") 218 except TclError: 219 first = last = None 220 if not first: 221 first = text.index("insert") 222 if not last: 223 last = first 224 return first, last 225 226def get_line_col(index): 227 '''Return (line, col) tuple of ints from 'line.col' string.''' 228 line, col = map(int, index.split(".")) # Fails on invalid index 229 return line, col 230 231if __name__ == "__main__": 232 import unittest 233 unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False) 234