1 import re 2 from Tkinter import * 3 import tkMessageBox 4 5 def get(root): 6 if not hasattr(root, "_searchengine"): 7 root._searchengine = SearchEngine(root) 8 # XXX This will never garbage-collect -- who cares 9 return root._searchengine 10 11 class SearchEngine: 12 13 def __init__(self, root): 14 self.root = root 15 # State shared by search, replace, and grep; 16 # the search dialogs bind these to UI elements. 17 self.patvar = StringVar(root) # search pattern 18 self.revar = BooleanVar(root) # regular expression? 19 self.casevar = BooleanVar(root) # match case? 20 self.wordvar = BooleanVar(root) # match whole word? 21 self.wrapvar = BooleanVar(root) # wrap around buffer? 22 self.wrapvar.set(1) # (on by default) 23 self.backvar = BooleanVar(root) # search backwards? 24 25 # Access methods 26 27 def getpat(self): 28 return self.patvar.get() 29 30 def setpat(self, pat): 31 self.patvar.set(pat) 32 33 def isre(self): 34 return self.revar.get() 35 36 def iscase(self): 37 return self.casevar.get() 38 39 def isword(self): 40 return self.wordvar.get() 41 42 def iswrap(self): 43 return self.wrapvar.get() 44 45 def isback(self): 46 return self.backvar.get() 47 48 # Higher level access methods 49 50 def getcookedpat(self): 51 pat = self.getpat() 52 if not self.isre(): 53 pat = re.escape(pat) 54 if self.isword(): 55 pat = r"\b%s\b" % pat 56 return pat 57 58 def getprog(self): 59 pat = self.getpat() 60 if not pat: 61 self.report_error(pat, "Empty regular expression") 62 return None 63 pat = self.getcookedpat() 64 flags = 0 65 if not self.iscase(): 66 flags = flags | re.IGNORECASE 67 try: 68 prog = re.compile(pat, flags) 69 except re.error, what: 70 try: 71 msg, col = what 72 except: 73 msg = str(what) 74 col = -1 75 self.report_error(pat, msg, col) 76 return None 77 return prog 78 79 def report_error(self, pat, msg, col=-1): 80 # Derived class could overrid this with something fancier 81 msg = "Error: " + str(msg) 82 if pat: 83 msg = msg + "\np\Pattern: " + str(pat) 84 if col >= 0: 85 msg = msg + "\nOffset: " + str(col) 86 tkMessageBox.showerror("Regular expression error", 87 msg, master=self.root) 88 89 def setcookedpat(self, pat): 90 if self.isre(): 91 pat = re.escape(pat) 92 self.setpat(pat) 93 94 def search_text(self, text, prog=None, ok=0): 95 """Search a text widget for the pattern. 96 97 If prog is given, it should be the precompiled pattern. 98 Return a tuple (lineno, matchobj); None if not found. 99 100 This obeys the wrap and direction (back) settings. 101 102 The search starts at the selection (if there is one) or 103 at the insert mark (otherwise). If the search is forward, 104 it starts at the right of the selection; for a backward 105 search, it starts at the left end. An empty match exactly 106 at either end of the selection (or at the insert mark if 107 there is no selection) is ignored unless the ok flag is true 108 -- this is done to guarantee progress. 109 110 If the search is allowed to wrap around, it will return the 111 original selection if (and only if) it is the only match. 112 113 """ 114 if not prog: 115 prog = self.getprog() 116 if not prog: 117 return None # Compilation failed -- stop 118 wrap = self.wrapvar.get() 119 first, last = get_selection(text) 120 if self.isback(): 121 if ok: 122 start = last 123 else: 124 start = first 125 line, col = get_line_col(start) 126 res = self.search_backward(text, prog, line, col, wrap, ok) 127 else: 128 if ok: 129 start = first 130 else: 131 start = last 132 line, col = get_line_col(start) 133 res = self.search_forward(text, prog, line, col, wrap, ok) 134 return res 135 136 def search_forward(self, text, prog, line, col, wrap, ok=0): 137 wrapped = 0 138 startline = line 139 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 140 while chars: 141 m = prog.search(chars[:-1], col) 142 if m: 143 if ok or m.end() > col: 144 return line, m 145 line = line + 1 146 if wrapped and line > startline: 147 break 148 col = 0 149 ok = 1 150 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 151 if not chars and wrap: 152 wrapped = 1 153 wrap = 0 154 line = 1 155 chars = text.get("1.0", "2.0") 156 return None 157 158 def search_backward(self, text, prog, line, col, wrap, ok=0): 159 wrapped = 0 160 startline = line 161 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 162 while 1: 163 m = search_reverse(prog, chars[:-1], col) 164 if m: 165 if ok or m.start() < col: 166 return line, m 167 line = line - 1 168 if wrapped and line < startline: 169 break 170 ok = 1 171 if line <= 0: 172 if not wrap: 173 break 174 wrapped = 1 175 wrap = 0 176 pos = text.index("end-1c") 177 line, col = map(int, pos.split(".")) 178 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 179 col = len(chars) - 1 180 return None 181 182 # Helper to search backwards in a string. 183 # (Optimized for the case where the pattern isn't found.) 184 185 def search_reverse(prog, chars, col): 186 m = prog.search(chars) 187 if not m: 188 return None 189 found = None 190 i, j = m.span() 191 while i < col and j <= col: 192 found = m 193 if i == j: 194 j = j+1 195 m = prog.search(chars, j) 196 if not m: 197 break 198 i, j = m.span() 199 return found 200 201 # Helper to get selection end points, defaulting to insert mark. 202 # Return a tuple of indices ("line.col" strings). 203 204 def get_selection(text): 205 try: 206 first = text.index("sel.first") 207 last = text.index("sel.last") 208 except TclError: 209 first = last = None 210 if not first: 211 first = text.index("insert") 212 if not last: 213 last = first 214 return first, last 215 216 # Helper to parse a text index into a (line, col) tuple. 217 218 def get_line_col(index): 219 line, col = map(int, index.split(".")) # Fails on invalid index 220 return line, col 221