comparison 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
comparison
equal deleted inserted replaced
67:0e9998148a16 69:33d812a61356
1 """Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
2 Uses idlelib.searchengine.SearchEngine for search capability.
3 Defines various replace related functions like replace, replace all,
4 and replace+find.
5 """
6 import re
7
8 from tkinter import StringVar, TclError
9
10 from idlelib.searchbase import SearchDialogBase
11 from idlelib import searchengine
12
13
14 def 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
31 class 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
269 def _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
302 if __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)