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