annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/replace.py @ 68:5028fdace37b

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