blob: 6be034af9626b30a7d7b54085fb651b24183aad3 [file] [log] [blame]
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -04001"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
Cheryl Sabella0bb5e752019-03-16 19:29:33 -04002Uses idlelib.searchengine.SearchEngine for search capability.
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -04003Defines various replace related functions like replace, replace all,
Cheryl Sabella0bb5e752019-03-16 19:29:33 -04004and replace+find.
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -04005"""
Andrew Svetlov5ad514d2012-08-04 21:38:22 +03006import re
7
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -04008from tkinter import StringVar, TclError
9
10from idlelib.searchbase import SearchDialogBase
11from idlelib import searchengine
David Scherer7aced172000-08-15 01:13:23 +000012
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040013
David Scherer7aced172000-08-15 01:13:23 +000014def replace(text):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040015 """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 """
David Scherer7aced172000-08-15 01:13:23 +000023 root = text._root()
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040024 engine = searchengine.get(root)
David Scherer7aced172000-08-15 01:13:23 +000025 if not hasattr(engine, "_replacedialog"):
26 engine._replacedialog = ReplaceDialog(root, engine)
27 dialog = engine._replacedialog
28 dialog.open(text)
29
Andrew Svetlov5ad514d2012-08-04 21:38:22 +030030
David Scherer7aced172000-08-15 01:13:23 +000031class ReplaceDialog(SearchDialogBase):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040032 "Dialog for finding and replacing a pattern in text."
David Scherer7aced172000-08-15 01:13:23 +000033
34 title = "Replace Dialog"
35 icon = "Replace"
36
37 def __init__(self, root, engine):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040038 """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)
David Scherer7aced172000-08-15 01:13:23 +000051 self.replvar = StringVar(root)
52
53 def open(self, text):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040054 """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 """
David Scherer7aced172000-08-15 01:13:23 +000062 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)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040074 self.ok = True
David Scherer7aced172000-08-15 01:13:23 +000075
76 def create_entries(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040077 "Create base and additional label and text entry widgets."
David Scherer7aced172000-08-15 01:13:23 +000078 SearchDialogBase.create_entries(self)
Terry Jan Reedy5283c4e2014-07-13 17:27:26 -040079 self.replent = self.make_entry("Replace with:", self.replvar)[0]
David Scherer7aced172000-08-15 01:13:23 +000080
81 def create_command_buttons(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040082 """Create base and additional command buttons.
83
84 The additional buttons are for Find, Replace,
85 Replace+Find, and Replace All.
86 """
David Scherer7aced172000-08-15 01:13:23 +000087 SearchDialogBase.create_command_buttons(self)
88 self.make_button("Find", self.find_it)
89 self.make_button("Replace", self.replace_it)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040090 self.make_button("Replace+Find", self.default_command, isdef=True)
David Scherer7aced172000-08-15 01:13:23 +000091 self.make_button("Replace All", self.replace_all)
92
93 def find_it(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040094 "Handle the Find button."
95 self.do_find(False)
David Scherer7aced172000-08-15 01:13:23 +000096
97 def replace_it(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040098 """Handle the Replace button.
99
100 If the find is successful, then perform replace.
101 """
David Scherer7aced172000-08-15 01:13:23 +0000102 if self.do_find(self.ok):
103 self.do_replace()
104
105 def default_command(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400106 """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 """
David Scherer7aced172000-08-15 01:13:23 +0000111 if self.do_find(self.ok):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400112 if self.do_replace(): # Only find next match if replace succeeded.
113 # A bad re can cause it to fail.
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400114 self.do_find(False)
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300115
116 def _replace_expand(self, m, repl):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400117 "Expand replacement text if regular expression."
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300118 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
David Scherer7aced172000-08-15 01:13:23 +0000128
129 def replace_all(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400130 """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 """
David Scherer7aced172000-08-15 01:13:23 +0000139 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:
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400146 self.bell()
David Scherer7aced172000-08-15 01:13:23 +0000147 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
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400155 ok = True
David Scherer7aced172000-08-15 01:13:23 +0000156 first = last = None
157 # XXX ought to replace circular instead of top-to-bottom when wrapping
158 text.undo_block_start()
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400159 while True:
160 res = self.engine.search_forward(text, prog, line, col,
161 wrap=False, ok=ok)
David Scherer7aced172000-08-15 01:13:23 +0000162 if not res:
163 break
164 line, m = res
165 chars = text.get("%d.0" % line, "%d.0" % (line+1))
166 orig = m.group()
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300167 new = self._replace_expand(m, repl)
168 if new is None:
169 break
David Scherer7aced172000-08-15 01:13:23 +0000170 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)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400182 ok = False
David Scherer7aced172000-08-15 01:13:23 +0000183 text.undo_block_stop()
184 if first and last:
185 self.show_hit(first, last)
Benjamin Petersoncf658c22013-04-03 22:35:12 -0400186 self.close()
David Scherer7aced172000-08-15 01:13:23 +0000187
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400188 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 """
David Scherer7aced172000-08-15 01:13:23 +0000193 if not self.engine.getprog():
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000194 return False
David Scherer7aced172000-08-15 01:13:23 +0000195 text = self.text
196 res = self.engine.search_text(text, None, ok)
197 if not res:
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400198 self.bell()
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000199 return False
David Scherer7aced172000-08-15 01:13:23 +0000200 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)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400205 self.ok = True
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000206 return True
David Scherer7aced172000-08-15 01:13:23 +0000207
208 def do_replace(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400209 "Replace search pattern in text with replacement value."
David Scherer7aced172000-08-15 01:13:23 +0000210 prog = self.engine.getprog()
211 if not prog:
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000212 return False
David Scherer7aced172000-08-15 01:13:23 +0000213 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")
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400221 line, col = searchengine.get_line_col(pos)
David Scherer7aced172000-08-15 01:13:23 +0000222 chars = text.get("%d.0" % line, "%d.0" % (line+1))
223 m = prog.match(chars, col)
224 if not prog:
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000225 return False
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300226 new = self._replace_expand(m, self.replvar.get())
227 if new is None:
228 return False
David Scherer7aced172000-08-15 01:13:23 +0000229 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"))
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400237 self.ok = False
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000238 return True
David Scherer7aced172000-08-15 01:13:23 +0000239
David Scherer7aced172000-08-15 01:13:23 +0000240 def show_hit(self, first, last):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400241 """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 """
David Scherer7aced172000-08-15 01:13:23 +0000251 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):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400264 "Close the dialog and remove hit tags."
David Scherer7aced172000-08-15 01:13:23 +0000265 SearchDialogBase.close(self, event)
266 self.text.tag_remove("hit", "1.0", "end")
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400267
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400268
269def _replace_dialog(parent): # htest #
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400270 from tkinter import Toplevel, Text, END, SEL
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500271 from tkinter.ttk import Frame, Button
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -0400272
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500273 top = Toplevel(parent)
274 top.title("Test ReplaceDialog")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400275 x, y = map(int, parent.geometry().split('+')[1:])
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500276 top.geometry("+%d+%d" % (x, y + 175))
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400277
278 # mock undo delegator methods
279 def undo_block_start():
280 pass
281
282 def undo_block_stop():
283 pass
284
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500285 frame = Frame(top)
286 frame.pack()
287 text = Text(frame, inactiveselectbackground='gray')
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400288 text.undo_block_start = undo_block_start
289 text.undo_block_stop = undo_block_stop
290 text.pack()
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400291 text.insert("insert","This is a sample sTring\nPlus MORE.")
292 text.focus_set()
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400293
294 def show_replace():
295 text.tag_add(SEL, "1.0", END)
296 replace(text)
297 text.tag_remove(SEL, "1.0", END)
298
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500299 button = Button(frame, text="Replace", command=show_replace)
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400300 button.pack()
301
302if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400303 from unittest import main
304 main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400305
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400306 from idlelib.idle_test.htest import run
307 run(_replace_dialog)