jpayne@69
|
1 """Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
|
jpayne@69
|
2 Uses idlelib.searchengine.SearchEngine for search capability.
|
jpayne@69
|
3 Defines various replace related functions like replace, replace all,
|
jpayne@69
|
4 and replace+find.
|
jpayne@69
|
5 """
|
jpayne@69
|
6 import re
|
jpayne@69
|
7
|
jpayne@69
|
8 from tkinter import StringVar, TclError
|
jpayne@69
|
9
|
jpayne@69
|
10 from idlelib.searchbase import SearchDialogBase
|
jpayne@69
|
11 from idlelib import searchengine
|
jpayne@69
|
12
|
jpayne@69
|
13
|
jpayne@69
|
14 def replace(text):
|
jpayne@69
|
15 """Create or reuse a singleton ReplaceDialog instance.
|
jpayne@69
|
16
|
jpayne@69
|
17 The singleton dialog saves user entries and preferences
|
jpayne@69
|
18 across instances.
|
jpayne@69
|
19
|
jpayne@69
|
20 Args:
|
jpayne@69
|
21 text: Text widget containing the text to be searched.
|
jpayne@69
|
22 """
|
jpayne@69
|
23 root = text._root()
|
jpayne@69
|
24 engine = searchengine.get(root)
|
jpayne@69
|
25 if not hasattr(engine, "_replacedialog"):
|
jpayne@69
|
26 engine._replacedialog = ReplaceDialog(root, engine)
|
jpayne@69
|
27 dialog = engine._replacedialog
|
jpayne@69
|
28 dialog.open(text)
|
jpayne@69
|
29
|
jpayne@69
|
30
|
jpayne@69
|
31 class ReplaceDialog(SearchDialogBase):
|
jpayne@69
|
32 "Dialog for finding and replacing a pattern in text."
|
jpayne@69
|
33
|
jpayne@69
|
34 title = "Replace Dialog"
|
jpayne@69
|
35 icon = "Replace"
|
jpayne@69
|
36
|
jpayne@69
|
37 def __init__(self, root, engine):
|
jpayne@69
|
38 """Create search dialog for finding and replacing text.
|
jpayne@69
|
39
|
jpayne@69
|
40 Uses SearchDialogBase as the basis for the GUI and a
|
jpayne@69
|
41 searchengine instance to prepare the search.
|
jpayne@69
|
42
|
jpayne@69
|
43 Attributes:
|
jpayne@69
|
44 replvar: StringVar containing 'Replace with:' value.
|
jpayne@69
|
45 replent: Entry widget for replvar. Created in
|
jpayne@69
|
46 create_entries().
|
jpayne@69
|
47 ok: Boolean used in searchengine.search_text to indicate
|
jpayne@69
|
48 whether the search includes the selection.
|
jpayne@69
|
49 """
|
jpayne@69
|
50 super().__init__(root, engine)
|
jpayne@69
|
51 self.replvar = StringVar(root)
|
jpayne@69
|
52
|
jpayne@69
|
53 def open(self, text):
|
jpayne@69
|
54 """Make dialog visible on top of others and ready to use.
|
jpayne@69
|
55
|
jpayne@69
|
56 Also, highlight the currently selected text and set the
|
jpayne@69
|
57 search to include the current selection (self.ok).
|
jpayne@69
|
58
|
jpayne@69
|
59 Args:
|
jpayne@69
|
60 text: Text widget being searched.
|
jpayne@69
|
61 """
|
jpayne@69
|
62 SearchDialogBase.open(self, text)
|
jpayne@69
|
63 try:
|
jpayne@69
|
64 first = text.index("sel.first")
|
jpayne@69
|
65 except TclError:
|
jpayne@69
|
66 first = None
|
jpayne@69
|
67 try:
|
jpayne@69
|
68 last = text.index("sel.last")
|
jpayne@69
|
69 except TclError:
|
jpayne@69
|
70 last = None
|
jpayne@69
|
71 first = first or text.index("insert")
|
jpayne@69
|
72 last = last or first
|
jpayne@69
|
73 self.show_hit(first, last)
|
jpayne@69
|
74 self.ok = True
|
jpayne@69
|
75
|
jpayne@69
|
76 def create_entries(self):
|
jpayne@69
|
77 "Create base and additional label and text entry widgets."
|
jpayne@69
|
78 SearchDialogBase.create_entries(self)
|
jpayne@69
|
79 self.replent = self.make_entry("Replace with:", self.replvar)[0]
|
jpayne@69
|
80
|
jpayne@69
|
81 def create_command_buttons(self):
|
jpayne@69
|
82 """Create base and additional command buttons.
|
jpayne@69
|
83
|
jpayne@69
|
84 The additional buttons are for Find, Replace,
|
jpayne@69
|
85 Replace+Find, and Replace All.
|
jpayne@69
|
86 """
|
jpayne@69
|
87 SearchDialogBase.create_command_buttons(self)
|
jpayne@69
|
88 self.make_button("Find", self.find_it)
|
jpayne@69
|
89 self.make_button("Replace", self.replace_it)
|
jpayne@69
|
90 self.make_button("Replace+Find", self.default_command, isdef=True)
|
jpayne@69
|
91 self.make_button("Replace All", self.replace_all)
|
jpayne@69
|
92
|
jpayne@69
|
93 def find_it(self, event=None):
|
jpayne@69
|
94 "Handle the Find button."
|
jpayne@69
|
95 self.do_find(False)
|
jpayne@69
|
96
|
jpayne@69
|
97 def replace_it(self, event=None):
|
jpayne@69
|
98 """Handle the Replace button.
|
jpayne@69
|
99
|
jpayne@69
|
100 If the find is successful, then perform replace.
|
jpayne@69
|
101 """
|
jpayne@69
|
102 if self.do_find(self.ok):
|
jpayne@69
|
103 self.do_replace()
|
jpayne@69
|
104
|
jpayne@69
|
105 def default_command(self, event=None):
|
jpayne@69
|
106 """Handle the Replace+Find button as the default command.
|
jpayne@69
|
107
|
jpayne@69
|
108 First performs a replace and then, if the replace was
|
jpayne@69
|
109 successful, a find next.
|
jpayne@69
|
110 """
|
jpayne@69
|
111 if self.do_find(self.ok):
|
jpayne@69
|
112 if self.do_replace(): # Only find next match if replace succeeded.
|
jpayne@69
|
113 # A bad re can cause it to fail.
|
jpayne@69
|
114 self.do_find(False)
|
jpayne@69
|
115
|
jpayne@69
|
116 def _replace_expand(self, m, repl):
|
jpayne@69
|
117 "Expand replacement text if regular expression."
|
jpayne@69
|
118 if self.engine.isre():
|
jpayne@69
|
119 try:
|
jpayne@69
|
120 new = m.expand(repl)
|
jpayne@69
|
121 except re.error:
|
jpayne@69
|
122 self.engine.report_error(repl, 'Invalid Replace Expression')
|
jpayne@69
|
123 new = None
|
jpayne@69
|
124 else:
|
jpayne@69
|
125 new = repl
|
jpayne@69
|
126
|
jpayne@69
|
127 return new
|
jpayne@69
|
128
|
jpayne@69
|
129 def replace_all(self, event=None):
|
jpayne@69
|
130 """Handle the Replace All button.
|
jpayne@69
|
131
|
jpayne@69
|
132 Search text for occurrences of the Find value and replace
|
jpayne@69
|
133 each of them. The 'wrap around' value controls the start
|
jpayne@69
|
134 point for searching. If wrap isn't set, then the searching
|
jpayne@69
|
135 starts at the first occurrence after the current selection;
|
jpayne@69
|
136 if wrap is set, the replacement starts at the first line.
|
jpayne@69
|
137 The replacement is always done top-to-bottom in the text.
|
jpayne@69
|
138 """
|
jpayne@69
|
139 prog = self.engine.getprog()
|
jpayne@69
|
140 if not prog:
|
jpayne@69
|
141 return
|
jpayne@69
|
142 repl = self.replvar.get()
|
jpayne@69
|
143 text = self.text
|
jpayne@69
|
144 res = self.engine.search_text(text, prog)
|
jpayne@69
|
145 if not res:
|
jpayne@69
|
146 self.bell()
|
jpayne@69
|
147 return
|
jpayne@69
|
148 text.tag_remove("sel", "1.0", "end")
|
jpayne@69
|
149 text.tag_remove("hit", "1.0", "end")
|
jpayne@69
|
150 line = res[0]
|
jpayne@69
|
151 col = res[1].start()
|
jpayne@69
|
152 if self.engine.iswrap():
|
jpayne@69
|
153 line = 1
|
jpayne@69
|
154 col = 0
|
jpayne@69
|
155 ok = True
|
jpayne@69
|
156 first = last = None
|
jpayne@69
|
157 # XXX ought to replace circular instead of top-to-bottom when wrapping
|
jpayne@69
|
158 text.undo_block_start()
|
jpayne@69
|
159 while True:
|
jpayne@69
|
160 res = self.engine.search_forward(text, prog, line, col,
|
jpayne@69
|
161 wrap=False, ok=ok)
|
jpayne@69
|
162 if not res:
|
jpayne@69
|
163 break
|
jpayne@69
|
164 line, m = res
|
jpayne@69
|
165 chars = text.get("%d.0" % line, "%d.0" % (line+1))
|
jpayne@69
|
166 orig = m.group()
|
jpayne@69
|
167 new = self._replace_expand(m, repl)
|
jpayne@69
|
168 if new is None:
|
jpayne@69
|
169 break
|
jpayne@69
|
170 i, j = m.span()
|
jpayne@69
|
171 first = "%d.%d" % (line, i)
|
jpayne@69
|
172 last = "%d.%d" % (line, j)
|
jpayne@69
|
173 if new == orig:
|
jpayne@69
|
174 text.mark_set("insert", last)
|
jpayne@69
|
175 else:
|
jpayne@69
|
176 text.mark_set("insert", first)
|
jpayne@69
|
177 if first != last:
|
jpayne@69
|
178 text.delete(first, last)
|
jpayne@69
|
179 if new:
|
jpayne@69
|
180 text.insert(first, new)
|
jpayne@69
|
181 col = i + len(new)
|
jpayne@69
|
182 ok = False
|
jpayne@69
|
183 text.undo_block_stop()
|
jpayne@69
|
184 if first and last:
|
jpayne@69
|
185 self.show_hit(first, last)
|
jpayne@69
|
186 self.close()
|
jpayne@69
|
187
|
jpayne@69
|
188 def do_find(self, ok=False):
|
jpayne@69
|
189 """Search for and highlight next occurrence of pattern in text.
|
jpayne@69
|
190
|
jpayne@69
|
191 No text replacement is done with this option.
|
jpayne@69
|
192 """
|
jpayne@69
|
193 if not self.engine.getprog():
|
jpayne@69
|
194 return False
|
jpayne@69
|
195 text = self.text
|
jpayne@69
|
196 res = self.engine.search_text(text, None, ok)
|
jpayne@69
|
197 if not res:
|
jpayne@69
|
198 self.bell()
|
jpayne@69
|
199 return False
|
jpayne@69
|
200 line, m = res
|
jpayne@69
|
201 i, j = m.span()
|
jpayne@69
|
202 first = "%d.%d" % (line, i)
|
jpayne@69
|
203 last = "%d.%d" % (line, j)
|
jpayne@69
|
204 self.show_hit(first, last)
|
jpayne@69
|
205 self.ok = True
|
jpayne@69
|
206 return True
|
jpayne@69
|
207
|
jpayne@69
|
208 def do_replace(self):
|
jpayne@69
|
209 "Replace search pattern in text with replacement value."
|
jpayne@69
|
210 prog = self.engine.getprog()
|
jpayne@69
|
211 if not prog:
|
jpayne@69
|
212 return False
|
jpayne@69
|
213 text = self.text
|
jpayne@69
|
214 try:
|
jpayne@69
|
215 first = pos = text.index("sel.first")
|
jpayne@69
|
216 last = text.index("sel.last")
|
jpayne@69
|
217 except TclError:
|
jpayne@69
|
218 pos = None
|
jpayne@69
|
219 if not pos:
|
jpayne@69
|
220 first = last = pos = text.index("insert")
|
jpayne@69
|
221 line, col = searchengine.get_line_col(pos)
|
jpayne@69
|
222 chars = text.get("%d.0" % line, "%d.0" % (line+1))
|
jpayne@69
|
223 m = prog.match(chars, col)
|
jpayne@69
|
224 if not prog:
|
jpayne@69
|
225 return False
|
jpayne@69
|
226 new = self._replace_expand(m, self.replvar.get())
|
jpayne@69
|
227 if new is None:
|
jpayne@69
|
228 return False
|
jpayne@69
|
229 text.mark_set("insert", first)
|
jpayne@69
|
230 text.undo_block_start()
|
jpayne@69
|
231 if m.group():
|
jpayne@69
|
232 text.delete(first, last)
|
jpayne@69
|
233 if new:
|
jpayne@69
|
234 text.insert(first, new)
|
jpayne@69
|
235 text.undo_block_stop()
|
jpayne@69
|
236 self.show_hit(first, text.index("insert"))
|
jpayne@69
|
237 self.ok = False
|
jpayne@69
|
238 return True
|
jpayne@69
|
239
|
jpayne@69
|
240 def show_hit(self, first, last):
|
jpayne@69
|
241 """Highlight text between first and last indices.
|
jpayne@69
|
242
|
jpayne@69
|
243 Text is highlighted via the 'hit' tag and the marked
|
jpayne@69
|
244 section is brought into view.
|
jpayne@69
|
245
|
jpayne@69
|
246 The colors from the 'hit' tag aren't currently shown
|
jpayne@69
|
247 when the text is displayed. This is due to the 'sel'
|
jpayne@69
|
248 tag being added first, so the colors in the 'sel'
|
jpayne@69
|
249 config are seen instead of the colors for 'hit'.
|
jpayne@69
|
250 """
|
jpayne@69
|
251 text = self.text
|
jpayne@69
|
252 text.mark_set("insert", first)
|
jpayne@69
|
253 text.tag_remove("sel", "1.0", "end")
|
jpayne@69
|
254 text.tag_add("sel", first, last)
|
jpayne@69
|
255 text.tag_remove("hit", "1.0", "end")
|
jpayne@69
|
256 if first == last:
|
jpayne@69
|
257 text.tag_add("hit", first)
|
jpayne@69
|
258 else:
|
jpayne@69
|
259 text.tag_add("hit", first, last)
|
jpayne@69
|
260 text.see("insert")
|
jpayne@69
|
261 text.update_idletasks()
|
jpayne@69
|
262
|
jpayne@69
|
263 def close(self, event=None):
|
jpayne@69
|
264 "Close the dialog and remove hit tags."
|
jpayne@69
|
265 SearchDialogBase.close(self, event)
|
jpayne@69
|
266 self.text.tag_remove("hit", "1.0", "end")
|
jpayne@69
|
267
|
jpayne@69
|
268
|
jpayne@69
|
269 def _replace_dialog(parent): # htest #
|
jpayne@69
|
270 from tkinter import Toplevel, Text, END, SEL
|
jpayne@69
|
271 from tkinter.ttk import Frame, Button
|
jpayne@69
|
272
|
jpayne@69
|
273 top = Toplevel(parent)
|
jpayne@69
|
274 top.title("Test ReplaceDialog")
|
jpayne@69
|
275 x, y = map(int, parent.geometry().split('+')[1:])
|
jpayne@69
|
276 top.geometry("+%d+%d" % (x, y + 175))
|
jpayne@69
|
277
|
jpayne@69
|
278 # mock undo delegator methods
|
jpayne@69
|
279 def undo_block_start():
|
jpayne@69
|
280 pass
|
jpayne@69
|
281
|
jpayne@69
|
282 def undo_block_stop():
|
jpayne@69
|
283 pass
|
jpayne@69
|
284
|
jpayne@69
|
285 frame = Frame(top)
|
jpayne@69
|
286 frame.pack()
|
jpayne@69
|
287 text = Text(frame, inactiveselectbackground='gray')
|
jpayne@69
|
288 text.undo_block_start = undo_block_start
|
jpayne@69
|
289 text.undo_block_stop = undo_block_stop
|
jpayne@69
|
290 text.pack()
|
jpayne@69
|
291 text.insert("insert","This is a sample sTring\nPlus MORE.")
|
jpayne@69
|
292 text.focus_set()
|
jpayne@69
|
293
|
jpayne@69
|
294 def show_replace():
|
jpayne@69
|
295 text.tag_add(SEL, "1.0", END)
|
jpayne@69
|
296 replace(text)
|
jpayne@69
|
297 text.tag_remove(SEL, "1.0", END)
|
jpayne@69
|
298
|
jpayne@69
|
299 button = Button(frame, text="Replace", command=show_replace)
|
jpayne@69
|
300 button.pack()
|
jpayne@69
|
301
|
jpayne@69
|
302 if __name__ == '__main__':
|
jpayne@69
|
303 from unittest import main
|
jpayne@69
|
304 main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
|
jpayne@69
|
305
|
jpayne@69
|
306 from idlelib.idle_test.htest import run
|
jpayne@69
|
307 run(_replace_dialog)
|