annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/replace.py @ 69:33d812a61356

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