blob: 2f9ca231a05e49510c699695a3b5b4bc6b9c63e2 [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
Tal Einat15d38612021-04-29 01:27:55 +030014def replace(text, insert_tags=None):
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
Tal Einat15d38612021-04-29 01:27:55 +030028 dialog.open(text, insert_tags=insert_tags)
David Scherer7aced172000-08-15 01:13:23 +000029
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)
Tal Einat15d38612021-04-29 01:27:55 +030052 self.insert_tags = None
David Scherer7aced172000-08-15 01:13:23 +000053
Tal Einat15d38612021-04-29 01:27:55 +030054 def open(self, text, insert_tags=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040055 """Make dialog visible on top of others and ready to use.
56
57 Also, highlight the currently selected text and set the
58 search to include the current selection (self.ok).
59
60 Args:
61 text: Text widget being searched.
62 """
David Scherer7aced172000-08-15 01:13:23 +000063 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)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040075 self.ok = True
Tal Einat15d38612021-04-29 01:27:55 +030076 self.insert_tags = insert_tags
David Scherer7aced172000-08-15 01:13:23 +000077
78 def create_entries(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040079 "Create base and additional label and text entry widgets."
David Scherer7aced172000-08-15 01:13:23 +000080 SearchDialogBase.create_entries(self)
Terry Jan Reedy5283c4e2014-07-13 17:27:26 -040081 self.replent = self.make_entry("Replace with:", self.replvar)[0]
David Scherer7aced172000-08-15 01:13:23 +000082
83 def create_command_buttons(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040084 """Create base and additional command buttons.
85
86 The additional buttons are for Find, Replace,
87 Replace+Find, and Replace All.
88 """
David Scherer7aced172000-08-15 01:13:23 +000089 SearchDialogBase.create_command_buttons(self)
90 self.make_button("Find", self.find_it)
91 self.make_button("Replace", self.replace_it)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040092 self.make_button("Replace+Find", self.default_command, isdef=True)
David Scherer7aced172000-08-15 01:13:23 +000093 self.make_button("Replace All", self.replace_all)
94
95 def find_it(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -040096 "Handle the Find button."
97 self.do_find(False)
David Scherer7aced172000-08-15 01:13:23 +000098
99 def replace_it(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400100 """Handle the Replace button.
101
102 If the find is successful, then perform replace.
103 """
David Scherer7aced172000-08-15 01:13:23 +0000104 if self.do_find(self.ok):
105 self.do_replace()
106
107 def default_command(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400108 """Handle the Replace+Find button as the default command.
109
110 First performs a replace and then, if the replace was
111 successful, a find next.
112 """
David Scherer7aced172000-08-15 01:13:23 +0000113 if self.do_find(self.ok):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400114 if self.do_replace(): # Only find next match if replace succeeded.
115 # A bad re can cause it to fail.
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400116 self.do_find(False)
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300117
118 def _replace_expand(self, m, repl):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400119 "Expand replacement text if regular expression."
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300120 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
128
129 return new
David Scherer7aced172000-08-15 01:13:23 +0000130
131 def replace_all(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400132 """Handle the Replace All button.
133
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 """
David Scherer7aced172000-08-15 01:13:23 +0000141 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:
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400148 self.bell()
David Scherer7aced172000-08-15 01:13:23 +0000149 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
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400157 ok = True
David Scherer7aced172000-08-15 01:13:23 +0000158 first = last = None
159 # XXX ought to replace circular instead of top-to-bottom when wrapping
160 text.undo_block_start()
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400161 while True:
162 res = self.engine.search_forward(text, prog, line, col,
163 wrap=False, ok=ok)
David Scherer7aced172000-08-15 01:13:23 +0000164 if not res:
165 break
166 line, m = res
167 chars = text.get("%d.0" % line, "%d.0" % (line+1))
168 orig = m.group()
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300169 new = self._replace_expand(m, repl)
170 if new is None:
171 break
David Scherer7aced172000-08-15 01:13:23 +0000172 i, j = m.span()
173 first = "%d.%d" % (line, i)
174 last = "%d.%d" % (line, j)
175 if new == orig:
176 text.mark_set("insert", last)
177 else:
178 text.mark_set("insert", first)
179 if first != last:
180 text.delete(first, last)
181 if new:
Tal Einat15d38612021-04-29 01:27:55 +0300182 text.insert(first, new, self.insert_tags)
David Scherer7aced172000-08-15 01:13:23 +0000183 col = i + len(new)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400184 ok = False
David Scherer7aced172000-08-15 01:13:23 +0000185 text.undo_block_stop()
186 if first and last:
187 self.show_hit(first, last)
Benjamin Petersoncf658c22013-04-03 22:35:12 -0400188 self.close()
David Scherer7aced172000-08-15 01:13:23 +0000189
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400190 def do_find(self, ok=False):
191 """Search for and highlight next occurrence of pattern in text.
192
193 No text replacement is done with this option.
194 """
David Scherer7aced172000-08-15 01:13:23 +0000195 if not self.engine.getprog():
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000196 return False
David Scherer7aced172000-08-15 01:13:23 +0000197 text = self.text
198 res = self.engine.search_text(text, None, ok)
199 if not res:
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400200 self.bell()
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000201 return False
David Scherer7aced172000-08-15 01:13:23 +0000202 line, m = res
203 i, j = m.span()
204 first = "%d.%d" % (line, i)
205 last = "%d.%d" % (line, j)
206 self.show_hit(first, last)
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400207 self.ok = True
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000208 return True
David Scherer7aced172000-08-15 01:13:23 +0000209
210 def do_replace(self):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400211 "Replace search pattern in text with replacement value."
David Scherer7aced172000-08-15 01:13:23 +0000212 prog = self.engine.getprog()
213 if not prog:
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000214 return False
David Scherer7aced172000-08-15 01:13:23 +0000215 text = self.text
216 try:
217 first = pos = text.index("sel.first")
218 last = text.index("sel.last")
219 except TclError:
220 pos = None
221 if not pos:
222 first = last = pos = text.index("insert")
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400223 line, col = searchengine.get_line_col(pos)
David Scherer7aced172000-08-15 01:13:23 +0000224 chars = text.get("%d.0" % line, "%d.0" % (line+1))
225 m = prog.match(chars, col)
226 if not prog:
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000227 return False
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300228 new = self._replace_expand(m, self.replvar.get())
229 if new is None:
230 return False
David Scherer7aced172000-08-15 01:13:23 +0000231 text.mark_set("insert", first)
232 text.undo_block_start()
233 if m.group():
234 text.delete(first, last)
235 if new:
Tal Einat15d38612021-04-29 01:27:55 +0300236 text.insert(first, new, self.insert_tags)
David Scherer7aced172000-08-15 01:13:23 +0000237 text.undo_block_stop()
238 self.show_hit(first, text.index("insert"))
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400239 self.ok = False
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000240 return True
David Scherer7aced172000-08-15 01:13:23 +0000241
David Scherer7aced172000-08-15 01:13:23 +0000242 def show_hit(self, first, last):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400243 """Highlight text between first and last indices.
244
245 Text is highlighted via the 'hit' tag and the marked
246 section is brought into view.
247
248 The colors from the 'hit' tag aren't currently shown
249 when the text is displayed. This is due to the 'sel'
250 tag being added first, so the colors in the 'sel'
251 config are seen instead of the colors for 'hit'.
252 """
David Scherer7aced172000-08-15 01:13:23 +0000253 text = self.text
254 text.mark_set("insert", first)
255 text.tag_remove("sel", "1.0", "end")
256 text.tag_add("sel", first, last)
257 text.tag_remove("hit", "1.0", "end")
258 if first == last:
259 text.tag_add("hit", first)
260 else:
261 text.tag_add("hit", first, last)
262 text.see("insert")
263 text.update_idletasks()
264
265 def close(self, event=None):
Cheryl Sabella0bb5e752019-03-16 19:29:33 -0400266 "Close the dialog and remove hit tags."
David Scherer7aced172000-08-15 01:13:23 +0000267 SearchDialogBase.close(self, event)
268 self.text.tag_remove("hit", "1.0", "end")
Tal Einat15d38612021-04-29 01:27:55 +0300269 self.insert_tags = None
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400270
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400271
272def _replace_dialog(parent): # htest #
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400273 from tkinter import Toplevel, Text, END, SEL
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500274 from tkinter.ttk import Frame, Button
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -0400275
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500276 top = Toplevel(parent)
277 top.title("Test ReplaceDialog")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400278 x, y = map(int, parent.geometry().split('+')[1:])
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500279 top.geometry("+%d+%d" % (x, y + 175))
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400280
281 # mock undo delegator methods
282 def undo_block_start():
283 pass
284
285 def undo_block_stop():
286 pass
287
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500288 frame = Frame(top)
289 frame.pack()
290 text = Text(frame, inactiveselectbackground='gray')
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400291 text.undo_block_start = undo_block_start
292 text.undo_block_stop = undo_block_stop
293 text.pack()
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400294 text.insert("insert","This is a sample sTring\nPlus MORE.")
295 text.focus_set()
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400296
297 def show_replace():
298 text.tag_add(SEL, "1.0", END)
299 replace(text)
300 text.tag_remove(SEL, "1.0", END)
301
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -0500302 button = Button(frame, text="Replace", command=show_replace)
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400303 button.pack()
304
305if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400306 from unittest import main
307 main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400308
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400309 from idlelib.idle_test.htest import run
310 run(_replace_dialog)