1'''Define SearchEngine for search dialogs.''' 2import re 3 4from tkinter import StringVar, BooleanVar, TclError 5from tkinter import messagebox 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 e: 88 self.report_error(pat, e.msg, e.pos) 89 return None 90 return prog 91 92 def report_error(self, pat, msg, col=None): 93 # Derived class could override this with something fancier 94 msg = "Error: " + str(msg) 95 if pat: 96 msg = msg + "\nPattern: " + str(pat) 97 if col is not None: 98 msg = msg + "\nOffset: " + str(col) 99 messagebox.showerror("Regular expression error", 100 msg, master=self.root) 101 102 def search_text(self, text, prog=None, ok=0): 103 '''Return (lineno, matchobj) or None for forward/backward search. 104 105 This function calls the right function with the right arguments. 106 It directly return the result of that call. 107 108 Text is a text widget. Prog is a precompiled pattern. 109 The ok parameter is a bit complicated as it has two effects. 110 111 If there is a selection, the search begin at either end, 112 depending on the direction setting and ok, with ok meaning that 113 the search starts with the selection. Otherwise, search begins 114 at the insert mark. 115 116 To aid progress, the search functions do not return an empty 117 match at the starting position unless ok is True. 118 ''' 119 120 if not prog: 121 prog = self.getprog() 122 if not prog: 123 return None # Compilation failed -- stop 124 wrap = self.wrapvar.get() 125 first, last = get_selection(text) 126 if self.isback(): 127 if ok: 128 start = last 129 else: 130 start = first 131 line, col = get_line_col(start) 132 res = self.search_backward(text, prog, line, col, wrap, ok) 133 else: 134 if ok: 135 start = first 136 else: 137 start = last 138 line, col = get_line_col(start) 139 res = self.search_forward(text, prog, line, col, wrap, ok) 140 return res 141 142 def search_forward(self, text, prog, line, col, wrap, ok=0): 143 wrapped = 0 144 startline = line 145 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 146 while chars: 147 m = prog.search(chars[:-1], col) 148 if m: 149 if ok or m.end() > col: 150 return line, m 151 line = line + 1 152 if wrapped and line > startline: 153 break 154 col = 0 155 ok = 1 156 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 157 if not chars and wrap: 158 wrapped = 1 159 wrap = 0 160 line = 1 161 chars = text.get("1.0", "2.0") 162 return None 163 164 def search_backward(self, text, prog, line, col, wrap, ok=0): 165 wrapped = 0 166 startline = line 167 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 168 while 1: 169 m = search_reverse(prog, chars[:-1], col) 170 if m: 171 if ok or m.start() < col: 172 return line, m 173 line = line - 1 174 if wrapped and line < startline: 175 break 176 ok = 1 177 if line <= 0: 178 if not wrap: 179 break 180 wrapped = 1 181 wrap = 0 182 pos = text.index("end-1c") 183 line, col = map(int, pos.split(".")) 184 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 185 col = len(chars) - 1 186 return None 187 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 231 232if __name__ == "__main__": 233 from unittest import main 234 main('idlelib.idle_test.test_searchengine', verbosity=2) 235