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