blob: 83cf98756bdf8127978b8818ffe5cdfc96ea68ed [file] [log] [blame]
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -04001"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
2Uses idlelib.SearchEngine for search capability.
3Defines various replace related functions like replace, replace all,
4replace+find.
5"""
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
13def replace(text):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -040014 """Returns a singleton ReplaceDialog instance.The single dialog
15 saves user entries and preferences across instances."""
David Scherer7aced172000-08-15 01:13:23 +000016 root = text._root()
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040017 engine = searchengine.get(root)
David Scherer7aced172000-08-15 01:13:23 +000018 if not hasattr(engine, "_replacedialog"):
19 engine._replacedialog = ReplaceDialog(root, engine)
20 dialog = engine._replacedialog
21 dialog.open(text)
22
Andrew Svetlov5ad514d2012-08-04 21:38:22 +030023
David Scherer7aced172000-08-15 01:13:23 +000024class ReplaceDialog(SearchDialogBase):
25
26 title = "Replace Dialog"
27 icon = "Replace"
28
29 def __init__(self, root, engine):
30 SearchDialogBase.__init__(self, root, engine)
31 self.replvar = StringVar(root)
32
33 def open(self, text):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -040034 """Display the replace dialog"""
David Scherer7aced172000-08-15 01:13:23 +000035 SearchDialogBase.open(self, text)
36 try:
37 first = text.index("sel.first")
38 except TclError:
39 first = None
40 try:
41 last = text.index("sel.last")
42 except TclError:
43 last = None
44 first = first or text.index("insert")
45 last = last or first
46 self.show_hit(first, last)
47 self.ok = 1
48
49 def create_entries(self):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -040050 """Create label and text entry widgets"""
David Scherer7aced172000-08-15 01:13:23 +000051 SearchDialogBase.create_entries(self)
Terry Jan Reedy5283c4e2014-07-13 17:27:26 -040052 self.replent = self.make_entry("Replace with:", self.replvar)[0]
David Scherer7aced172000-08-15 01:13:23 +000053
54 def create_command_buttons(self):
55 SearchDialogBase.create_command_buttons(self)
56 self.make_button("Find", self.find_it)
57 self.make_button("Replace", self.replace_it)
58 self.make_button("Replace+Find", self.default_command, 1)
59 self.make_button("Replace All", self.replace_all)
60
61 def find_it(self, event=None):
62 self.do_find(0)
63
64 def replace_it(self, event=None):
65 if self.do_find(self.ok):
66 self.do_replace()
67
68 def default_command(self, event=None):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -040069 "Replace and find next."
David Scherer7aced172000-08-15 01:13:23 +000070 if self.do_find(self.ok):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -040071 if self.do_replace(): # Only find next match if replace succeeded.
72 # A bad re can cause it to fail.
Andrew Svetlov5ad514d2012-08-04 21:38:22 +030073 self.do_find(0)
74
75 def _replace_expand(self, m, repl):
76 """ Helper function for expanding a regular expression
77 in the replace field, if needed. """
78 if self.engine.isre():
79 try:
80 new = m.expand(repl)
81 except re.error:
82 self.engine.report_error(repl, 'Invalid Replace Expression')
83 new = None
84 else:
85 new = repl
86
87 return new
David Scherer7aced172000-08-15 01:13:23 +000088
89 def replace_all(self, event=None):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -040090 """Replace all instances of patvar with replvar in text"""
David Scherer7aced172000-08-15 01:13:23 +000091 prog = self.engine.getprog()
92 if not prog:
93 return
94 repl = self.replvar.get()
95 text = self.text
96 res = self.engine.search_text(text, prog)
97 if not res:
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -040098 self.bell()
David Scherer7aced172000-08-15 01:13:23 +000099 return
100 text.tag_remove("sel", "1.0", "end")
101 text.tag_remove("hit", "1.0", "end")
102 line = res[0]
103 col = res[1].start()
104 if self.engine.iswrap():
105 line = 1
106 col = 0
107 ok = 1
108 first = last = None
109 # XXX ought to replace circular instead of top-to-bottom when wrapping
110 text.undo_block_start()
111 while 1:
112 res = self.engine.search_forward(text, prog, line, col, 0, ok)
113 if not res:
114 break
115 line, m = res
116 chars = text.get("%d.0" % line, "%d.0" % (line+1))
117 orig = m.group()
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300118 new = self._replace_expand(m, repl)
119 if new is None:
120 break
David Scherer7aced172000-08-15 01:13:23 +0000121 i, j = m.span()
122 first = "%d.%d" % (line, i)
123 last = "%d.%d" % (line, j)
124 if new == orig:
125 text.mark_set("insert", last)
126 else:
127 text.mark_set("insert", first)
128 if first != last:
129 text.delete(first, last)
130 if new:
131 text.insert(first, new)
132 col = i + len(new)
133 ok = 0
134 text.undo_block_stop()
135 if first and last:
136 self.show_hit(first, last)
Benjamin Petersoncf658c22013-04-03 22:35:12 -0400137 self.close()
David Scherer7aced172000-08-15 01:13:23 +0000138
139 def do_find(self, ok=0):
140 if not self.engine.getprog():
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000141 return False
David Scherer7aced172000-08-15 01:13:23 +0000142 text = self.text
143 res = self.engine.search_text(text, None, ok)
144 if not res:
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400145 self.bell()
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000146 return False
David Scherer7aced172000-08-15 01:13:23 +0000147 line, m = res
148 i, j = m.span()
149 first = "%d.%d" % (line, i)
150 last = "%d.%d" % (line, j)
151 self.show_hit(first, last)
152 self.ok = 1
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000153 return True
David Scherer7aced172000-08-15 01:13:23 +0000154
155 def do_replace(self):
156 prog = self.engine.getprog()
157 if not prog:
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000158 return False
David Scherer7aced172000-08-15 01:13:23 +0000159 text = self.text
160 try:
161 first = pos = text.index("sel.first")
162 last = text.index("sel.last")
163 except TclError:
164 pos = None
165 if not pos:
166 first = last = pos = text.index("insert")
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400167 line, col = searchengine.get_line_col(pos)
David Scherer7aced172000-08-15 01:13:23 +0000168 chars = text.get("%d.0" % line, "%d.0" % (line+1))
169 m = prog.match(chars, col)
170 if not prog:
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000171 return False
Andrew Svetlov5ad514d2012-08-04 21:38:22 +0300172 new = self._replace_expand(m, self.replvar.get())
173 if new is None:
174 return False
David Scherer7aced172000-08-15 01:13:23 +0000175 text.mark_set("insert", first)
176 text.undo_block_start()
177 if m.group():
178 text.delete(first, last)
179 if new:
180 text.insert(first, new)
181 text.undo_block_stop()
182 self.show_hit(first, text.index("insert"))
183 self.ok = 0
Kurt B. Kaiserce86b102002-09-18 02:56:10 +0000184 return True
David Scherer7aced172000-08-15 01:13:23 +0000185
David Scherer7aced172000-08-15 01:13:23 +0000186 def show_hit(self, first, last):
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400187 """Highlight text from 'first' to 'last'.
188 'first', 'last' - Text indices"""
David Scherer7aced172000-08-15 01:13:23 +0000189 text = self.text
190 text.mark_set("insert", first)
191 text.tag_remove("sel", "1.0", "end")
192 text.tag_add("sel", first, last)
193 text.tag_remove("hit", "1.0", "end")
194 if first == last:
195 text.tag_add("hit", first)
196 else:
197 text.tag_add("hit", first, last)
198 text.see("insert")
199 text.update_idletasks()
200
201 def close(self, event=None):
202 SearchDialogBase.close(self, event)
203 self.text.tag_remove("hit", "1.0", "end")
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400204
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400205
206def _replace_dialog(parent): # htest #
Terry Jan Reedy3ff55a82016-08-10 23:44:54 -0400207 from tkinter import Toplevel, Text, END, SEL
208 from tkinter.ttk import Button
Terry Jan Reedy6f7b0f52016-07-10 20:21:31 -0400209
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400210 box = Toplevel(parent)
211 box.title("Test ReplaceDialog")
Terry Jan Reedya7480322016-07-10 17:28:10 -0400212 x, y = map(int, parent.geometry().split('+')[1:])
213 box.geometry("+%d+%d" % (x, y + 175))
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400214
215 # mock undo delegator methods
216 def undo_block_start():
217 pass
218
219 def undo_block_stop():
220 pass
221
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400222 text = Text(box, inactiveselectbackground='gray')
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400223 text.undo_block_start = undo_block_start
224 text.undo_block_stop = undo_block_stop
225 text.pack()
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400226 text.insert("insert","This is a sample sTring\nPlus MORE.")
227 text.focus_set()
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400228
229 def show_replace():
230 text.tag_add(SEL, "1.0", END)
231 replace(text)
232 text.tag_remove(SEL, "1.0", END)
233
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400234 button = Button(box, text="Replace", command=show_replace)
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400235 button.pack()
236
237if __name__ == '__main__':
Terry Jan Reedyea3dc802018-06-18 04:47:59 -0400238 from unittest import main
239 main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
Terry Jan Reedyfdec2a32016-05-17 19:58:02 -0400240
Terry Jan Reedy0a4d13e2014-05-27 03:30:54 -0400241 from idlelib.idle_test.htest import run
242 run(_replace_dialog)