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