1"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI. 2Uses idlelib.searchengine.SearchEngine for search capability. 3Defines various replace related functions like replace, replace all, 4and replace+find. 5""" 6import re 7 8from tkinter import StringVar, TclError 9 10from idlelib.searchbase import SearchDialogBase 11from idlelib import searchengine 12 13 14def replace(text, insert_tags=None): 15 """Create or reuse a singleton ReplaceDialog instance. 16 17 The singleton dialog saves user entries and preferences 18 across instances. 19 20 Args: 21 text: Text widget containing the text to be searched. 22 """ 23 root = text._root() 24 engine = searchengine.get(root) 25 if not hasattr(engine, "_replacedialog"): 26 engine._replacedialog = ReplaceDialog(root, engine) 27 dialog = engine._replacedialog 28 dialog.open(text, insert_tags=insert_tags) 29 30 31class ReplaceDialog(SearchDialogBase): 32 "Dialog for finding and replacing a pattern in text." 33 34 title = "Replace Dialog" 35 icon = "Replace" 36 37 def __init__(self, root, engine): 38 """Create search dialog for finding and replacing text. 39 40 Uses SearchDialogBase as the basis for the GUI and a 41 searchengine instance to prepare the search. 42 43 Attributes: 44 replvar: StringVar containing 'Replace with:' value. 45 replent: Entry widget for replvar. Created in 46 create_entries(). 47 ok: Boolean used in searchengine.search_text to indicate 48 whether the search includes the selection. 49 """ 50 super().__init__(root, engine) 51 self.replvar = StringVar(root) 52 self.insert_tags = None 53 54 def open(self, text, insert_tags=None): 55 """Make dialog visible on top of others and ready to use. 56 57 Also, highlight the currently selected text and set the 58 search to include the current selection (self.ok). 59 60 Args: 61 text: Text widget being searched. 62 """ 63 SearchDialogBase.open(self, text) 64 try: 65 first = text.index("sel.first") 66 except TclError: 67 first = None 68 try: 69 last = text.index("sel.last") 70 except TclError: 71 last = None 72 first = first or text.index("insert") 73 last = last or first 74 self.show_hit(first, last) 75 self.ok = True 76 self.insert_tags = insert_tags 77 78 def create_entries(self): 79 "Create base and additional label and text entry widgets." 80 SearchDialogBase.create_entries(self) 81 self.replent = self.make_entry("Replace with:", self.replvar)[0] 82 83 def create_command_buttons(self): 84 """Create base and additional command buttons. 85 86 The additional buttons are for Find, Replace, 87 Replace+Find, and Replace All. 88 """ 89 SearchDialogBase.create_command_buttons(self) 90 self.make_button("Find", self.find_it) 91 self.make_button("Replace", self.replace_it) 92 self.make_button("Replace+Find", self.default_command, isdef=True) 93 self.make_button("Replace All", self.replace_all) 94 95 def find_it(self, event=None): 96 "Handle the Find button." 97 self.do_find(False) 98 99 def replace_it(self, event=None): 100 """Handle the Replace button. 101 102 If the find is successful, then perform replace. 103 """ 104 if self.do_find(self.ok): 105 self.do_replace() 106 107 def default_command(self, event=None): 108 """Handle the Replace+Find button as the default command. 109 110 First performs a replace and then, if the replace was 111 successful, a find next. 112 """ 113 if self.do_find(self.ok): 114 if self.do_replace(): # Only find next match if replace succeeded. 115 # A bad re can cause it to fail. 116 self.do_find(False) 117 118 def _replace_expand(self, m, repl): 119 "Expand replacement text if regular expression." 120 if self.engine.isre(): 121 try: 122 new = m.expand(repl) 123 except re.error: 124 self.engine.report_error(repl, 'Invalid Replace Expression') 125 new = None 126 else: 127 new = repl 128 129 return new 130 131 def replace_all(self, event=None): 132 """Handle the Replace All button. 133 134 Search text for occurrences of the Find value and replace 135 each of them. The 'wrap around' value controls the start 136 point for searching. If wrap isn't set, then the searching 137 starts at the first occurrence after the current selection; 138 if wrap is set, the replacement starts at the first line. 139 The replacement is always done top-to-bottom in the text. 140 """ 141 prog = self.engine.getprog() 142 if not prog: 143 return 144 repl = self.replvar.get() 145 text = self.text 146 res = self.engine.search_text(text, prog) 147 if not res: 148 self.bell() 149 return 150 text.tag_remove("sel", "1.0", "end") 151 text.tag_remove("hit", "1.0", "end") 152 line = res[0] 153 col = res[1].start() 154 if self.engine.iswrap(): 155 line = 1 156 col = 0 157 ok = True 158 first = last = None 159 # XXX ought to replace circular instead of top-to-bottom when wrapping 160 text.undo_block_start() 161 while True: 162 res = self.engine.search_forward(text, prog, line, col, 163 wrap=False, ok=ok) 164 if not res: 165 break 166 line, m = res 167 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 168 orig = m.group() 169 new = self._replace_expand(m, repl) 170 if new is None: 171 break 172 i, j = m.span() 173 first = "%d.%d" % (line, i) 174 last = "%d.%d" % (line, j) 175 if new == orig: 176 text.mark_set("insert", last) 177 else: 178 text.mark_set("insert", first) 179 if first != last: 180 text.delete(first, last) 181 if new: 182 text.insert(first, new, self.insert_tags) 183 col = i + len(new) 184 ok = False 185 text.undo_block_stop() 186 if first and last: 187 self.show_hit(first, last) 188 self.close() 189 190 def do_find(self, ok=False): 191 """Search for and highlight next occurrence of pattern in text. 192 193 No text replacement is done with this option. 194 """ 195 if not self.engine.getprog(): 196 return False 197 text = self.text 198 res = self.engine.search_text(text, None, ok) 199 if not res: 200 self.bell() 201 return False 202 line, m = res 203 i, j = m.span() 204 first = "%d.%d" % (line, i) 205 last = "%d.%d" % (line, j) 206 self.show_hit(first, last) 207 self.ok = True 208 return True 209 210 def do_replace(self): 211 "Replace search pattern in text with replacement value." 212 prog = self.engine.getprog() 213 if not prog: 214 return False 215 text = self.text 216 try: 217 first = pos = text.index("sel.first") 218 last = text.index("sel.last") 219 except TclError: 220 pos = None 221 if not pos: 222 first = last = pos = text.index("insert") 223 line, col = searchengine.get_line_col(pos) 224 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 225 m = prog.match(chars, col) 226 if not prog: 227 return False 228 new = self._replace_expand(m, self.replvar.get()) 229 if new is None: 230 return False 231 text.mark_set("insert", first) 232 text.undo_block_start() 233 if m.group(): 234 text.delete(first, last) 235 if new: 236 text.insert(first, new, self.insert_tags) 237 text.undo_block_stop() 238 self.show_hit(first, text.index("insert")) 239 self.ok = False 240 return True 241 242 def show_hit(self, first, last): 243 """Highlight text between first and last indices. 244 245 Text is highlighted via the 'hit' tag and the marked 246 section is brought into view. 247 248 The colors from the 'hit' tag aren't currently shown 249 when the text is displayed. This is due to the 'sel' 250 tag being added first, so the colors in the 'sel' 251 config are seen instead of the colors for 'hit'. 252 """ 253 text = self.text 254 text.mark_set("insert", first) 255 text.tag_remove("sel", "1.0", "end") 256 text.tag_add("sel", first, last) 257 text.tag_remove("hit", "1.0", "end") 258 if first == last: 259 text.tag_add("hit", first) 260 else: 261 text.tag_add("hit", first, last) 262 text.see("insert") 263 text.update_idletasks() 264 265 def close(self, event=None): 266 "Close the dialog and remove hit tags." 267 SearchDialogBase.close(self, event) 268 self.text.tag_remove("hit", "1.0", "end") 269 self.insert_tags = None 270 271 272def _replace_dialog(parent): # htest # 273 from tkinter import Toplevel, Text, END, SEL 274 from tkinter.ttk import Frame, Button 275 276 top = Toplevel(parent) 277 top.title("Test ReplaceDialog") 278 x, y = map(int, parent.geometry().split('+')[1:]) 279 top.geometry("+%d+%d" % (x, y + 175)) 280 281 # mock undo delegator methods 282 def undo_block_start(): 283 pass 284 285 def undo_block_stop(): 286 pass 287 288 frame = Frame(top) 289 frame.pack() 290 text = Text(frame, inactiveselectbackground='gray') 291 text.undo_block_start = undo_block_start 292 text.undo_block_stop = undo_block_stop 293 text.pack() 294 text.insert("insert","This is a sample sTring\nPlus MORE.") 295 text.focus_set() 296 297 def show_replace(): 298 text.tag_add(SEL, "1.0", END) 299 replace(text) 300 text.tag_remove(SEL, "1.0", END) 301 302 button = Button(frame, text="Replace", command=show_replace) 303 button.pack() 304 305if __name__ == '__main__': 306 from unittest import main 307 main('idlelib.idle_test.test_replace', verbosity=2, exit=False) 308 309 from idlelib.idle_test.htest import run 310 run(_replace_dialog) 311