Coverage for replace.py: 10%
193 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
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
8from tkinter import StringVar, TclError
10from idlelib.searchbase import SearchDialogBase
11from idlelib import searchengine
14def replace(text, insert_tags=None):
15 """Create or reuse a singleton ReplaceDialog instance.
17 The singleton dialog saves user entries and preferences
18 across instances.
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)
31class ReplaceDialog(SearchDialogBase):
32 "Dialog for finding and replacing a pattern in text."
34 title = "Replace Dialog"
35 icon = "Replace"
37 def __init__(self, root, engine):
38 """Create search dialog for finding and replacing text.
40 Uses SearchDialogBase as the basis for the GUI and a
41 searchengine instance to prepare the search.
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
54 def open(self, text, insert_tags=None):
55 """Make dialog visible on top of others and ready to use.
57 Also, highlight the currently selected text and set the
58 search to include the current selection (self.ok).
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
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]
83 def create_command_buttons(self):
84 """Create base and additional command buttons.
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)
95 def find_it(self, event=None):
96 "Handle the Find button."
97 self.do_find(False)
99 def replace_it(self, event=None):
100 """Handle the Replace button.
102 If the find is successful, then perform replace.
103 """
104 if self.do_find(self.ok):
105 self.do_replace()
107 def default_command(self, event=None):
108 """Handle the Replace+Find button as the default command.
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)
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
129 return new
131 def replace_all(self, event=None):
132 """Handle the Replace All button.
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 res := self.engine.search_forward(
162 text, prog, line, col, wrap=False, ok=ok):
163 line, m = res
164 chars = text.get("%d.0" % line, "%d.0" % (line+1))
165 orig = m.group()
166 new = self._replace_expand(m, repl)
167 if new is None:
168 break
169 i, j = m.span()
170 first = "%d.%d" % (line, i)
171 last = "%d.%d" % (line, j)
172 if new == orig:
173 text.mark_set("insert", last)
174 else:
175 text.mark_set("insert", first)
176 if first != last:
177 text.delete(first, last)
178 if new:
179 text.insert(first, new, self.insert_tags)
180 col = i + len(new)
181 ok = False
182 text.undo_block_stop()
183 if first and last:
184 self.show_hit(first, last)
185 self.close()
187 def do_find(self, ok=False):
188 """Search for and highlight next occurrence of pattern in text.
190 No text replacement is done with this option.
191 """
192 if not self.engine.getprog():
193 return False
194 text = self.text
195 res = self.engine.search_text(text, None, ok)
196 if not res:
197 self.bell()
198 return False
199 line, m = res
200 i, j = m.span()
201 first = "%d.%d" % (line, i)
202 last = "%d.%d" % (line, j)
203 self.show_hit(first, last)
204 self.ok = True
205 return True
207 def do_replace(self):
208 "Replace search pattern in text with replacement value."
209 prog = self.engine.getprog()
210 if not prog:
211 return False
212 text = self.text
213 try:
214 first = pos = text.index("sel.first")
215 last = text.index("sel.last")
216 except TclError:
217 pos = None
218 if not pos:
219 first = last = pos = text.index("insert")
220 line, col = searchengine.get_line_col(pos)
221 chars = text.get("%d.0" % line, "%d.0" % (line+1))
222 m = prog.match(chars, col)
223 if not prog:
224 return False
225 new = self._replace_expand(m, self.replvar.get())
226 if new is None:
227 return False
228 text.mark_set("insert", first)
229 text.undo_block_start()
230 if m.group():
231 text.delete(first, last)
232 if new:
233 text.insert(first, new, self.insert_tags)
234 text.undo_block_stop()
235 self.show_hit(first, text.index("insert"))
236 self.ok = False
237 return True
239 def show_hit(self, first, last):
240 """Highlight text between first and last indices.
242 Text is highlighted via the 'hit' tag and the marked
243 section is brought into view.
245 The colors from the 'hit' tag aren't currently shown
246 when the text is displayed. This is due to the 'sel'
247 tag being added first, so the colors in the 'sel'
248 config are seen instead of the colors for 'hit'.
249 """
250 text = self.text
251 text.mark_set("insert", first)
252 text.tag_remove("sel", "1.0", "end")
253 text.tag_add("sel", first, last)
254 text.tag_remove("hit", "1.0", "end")
255 if first == last:
256 text.tag_add("hit", first)
257 else:
258 text.tag_add("hit", first, last)
259 text.see("insert")
260 text.update_idletasks()
262 def close(self, event=None):
263 "Close the dialog and remove hit tags."
264 SearchDialogBase.close(self, event)
265 self.text.tag_remove("hit", "1.0", "end")
266 self.insert_tags = None
269def _replace_dialog(parent): # htest #
270 from tkinter import Toplevel, Text, END, SEL
271 from tkinter.ttk import Frame, Button
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))
278 # mock undo delegator methods
279 def undo_block_start():
280 pass
282 def undo_block_stop():
283 pass
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()
294 def show_replace():
295 text.tag_add(SEL, "1.0", END)
296 replace(text)
297 text.tag_remove(SEL, "1.0", END)
299 button = Button(frame, text="Replace", command=show_replace)
300 button.pack()
302if __name__ == '__main__': 302 ↛ 303line 302 didn't jump to line 303, because the condition on line 302 was never true
303 from unittest import main
304 main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
306 from idlelib.idle_test.htest import run
307 run(_replace_dialog)