• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
2Uses idlelib.searchengine.SearchEngine for search capability.
3Defines various replace related functions like replace, replace all,
4and replace+find.
5"""
6import re
7
8from tkinter import StringVar, TclError
9
10from idlelib.searchbase import SearchDialogBase
11from idlelib import searchengine
12
13
14def replace(text, insert_tags=None):
15    """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    """
23    root = text._root()
24    engine = searchengine.get(root)
25    if not hasattr(engine, "_replacedialog"):
26        engine._replacedialog = ReplaceDialog(root, engine)
27    dialog = engine._replacedialog
28    dialog.open(text, insert_tags=insert_tags)
29
30
31class ReplaceDialog(SearchDialogBase):
32    "Dialog for finding and replacing a pattern in text."
33
34    title = "Replace Dialog"
35    icon = "Replace"
36
37    def __init__(self, root, engine):
38        """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)
51        self.replvar = StringVar(root)
52        self.insert_tags = None
53
54    def open(self, text, insert_tags=None):
55        """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        """
63        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)
75        self.ok = True
76        self.insert_tags = insert_tags
77
78    def create_entries(self):
79        "Create base and additional label and text entry widgets."
80        SearchDialogBase.create_entries(self)
81        self.replent = self.make_entry("Replace with:", self.replvar)[0]
82
83    def create_command_buttons(self):
84        """Create base and additional command buttons.
85
86        The additional buttons are for Find, Replace,
87        Replace+Find, and Replace All.
88        """
89        SearchDialogBase.create_command_buttons(self)
90        self.make_button("Find", self.find_it)
91        self.make_button("Replace", self.replace_it)
92        self.make_button("Replace+Find", self.default_command, isdef=True)
93        self.make_button("Replace All", self.replace_all)
94
95    def find_it(self, event=None):
96        "Handle the Find button."
97        self.do_find(False)
98
99    def replace_it(self, event=None):
100        """Handle the Replace button.
101
102        If the find is successful, then perform replace.
103        """
104        if self.do_find(self.ok):
105            self.do_replace()
106
107    def default_command(self, event=None):
108        """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        """
113        if self.do_find(self.ok):
114            if self.do_replace():  # Only find next match if replace succeeded.
115                                   # A bad re can cause it to fail.
116                self.do_find(False)
117
118    def _replace_expand(self, m, repl):
119        "Expand replacement text if regular expression."
120        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
130
131    def replace_all(self, event=None):
132        """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        """
141        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:
148            self.bell()
149            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
157        ok = True
158        first = last = None
159        # XXX ought to replace circular instead of top-to-bottom when wrapping
160        text.undo_block_start()
161        while res := self.engine.search_forward(
162                text, prog, line, col, wrap=False, ok=ok):
163            line, m = res
164            chars = text.get("%d.0" % line, "%d.0" % (line+1))
165            orig = m.group()
166            new = self._replace_expand(m, repl)
167            if new is None:
168                break
169            i, j = m.span()
170            first = "%d.%d" % (line, i)
171            last = "%d.%d" % (line, j)
172            if new == orig:
173                text.mark_set("insert", last)
174            else:
175                text.mark_set("insert", first)
176                if first != last:
177                    text.delete(first, last)
178                if new:
179                    text.insert(first, new, self.insert_tags)
180            col = i + len(new)
181            ok = False
182        text.undo_block_stop()
183        if first and last:
184            self.show_hit(first, last)
185        self.close()
186
187    def do_find(self, ok=False):
188        """Search for and highlight next occurrence of pattern in text.
189
190        No text replacement is done with this option.
191        """
192        if not self.engine.getprog():
193            return False
194        text = self.text
195        res = self.engine.search_text(text, None, ok)
196        if not res:
197            self.bell()
198            return False
199        line, m = res
200        i, j = m.span()
201        first = "%d.%d" % (line, i)
202        last = "%d.%d" % (line, j)
203        self.show_hit(first, last)
204        self.ok = True
205        return True
206
207    def do_replace(self):
208        "Replace search pattern in text with replacement value."
209        prog = self.engine.getprog()
210        if not prog:
211            return False
212        text = self.text
213        try:
214            first = pos = text.index("sel.first")
215            last = text.index("sel.last")
216        except TclError:
217            pos = None
218        if not pos:
219            first = last = pos = text.index("insert")
220        line, col = searchengine.get_line_col(pos)
221        chars = text.get("%d.0" % line, "%d.0" % (line+1))
222        m = prog.match(chars, col)
223        if not prog:
224            return False
225        new = self._replace_expand(m, self.replvar.get())
226        if new is None:
227            return False
228        text.mark_set("insert", first)
229        text.undo_block_start()
230        if m.group():
231            text.delete(first, last)
232        if new:
233            text.insert(first, new, self.insert_tags)
234        text.undo_block_stop()
235        self.show_hit(first, text.index("insert"))
236        self.ok = False
237        return True
238
239    def show_hit(self, first, last):
240        """Highlight text between first and last indices.
241
242        Text is highlighted via the 'hit' tag and the marked
243        section is brought into view.
244
245        The colors from the 'hit' tag aren't currently shown
246        when the text is displayed.  This is due to the 'sel'
247        tag being added first, so the colors in the 'sel'
248        config are seen instead of the colors for 'hit'.
249        """
250        text = self.text
251        text.mark_set("insert", first)
252        text.tag_remove("sel", "1.0", "end")
253        text.tag_add("sel", first, last)
254        text.tag_remove("hit", "1.0", "end")
255        if first == last:
256            text.tag_add("hit", first)
257        else:
258            text.tag_add("hit", first, last)
259        text.see("insert")
260        text.update_idletasks()
261
262    def close(self, event=None):
263        "Close the dialog and remove hit tags."
264        SearchDialogBase.close(self, event)
265        self.text.tag_remove("hit", "1.0", "end")
266        self.insert_tags = None
267
268
269def _replace_dialog(parent):  # htest #
270    from tkinter import Toplevel, Text, END, SEL
271    from tkinter.ttk import Frame, Button
272
273    top = Toplevel(parent)
274    top.title("Test ReplaceDialog")
275    x, y = map(int, parent.geometry().split('+')[1:])
276    top.geometry("+%d+%d" % (x, y + 175))
277
278    # mock undo delegator methods
279    def undo_block_start():
280        pass
281
282    def undo_block_stop():
283        pass
284
285    frame = Frame(top)
286    frame.pack()
287    text = Text(frame, inactiveselectbackground='gray')
288    text.undo_block_start = undo_block_start
289    text.undo_block_stop = undo_block_stop
290    text.pack()
291    text.insert("insert","This is a sample sTring\nPlus MORE.")
292    text.focus_set()
293
294    def show_replace():
295        text.tag_add(SEL, "1.0", END)
296        replace(text)
297        text.tag_remove(SEL, "1.0", END)
298
299    button = Button(frame, text="Replace", command=show_replace)
300    button.pack()
301
302if __name__ == '__main__':
303    from unittest import main
304    main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
305
306    from idlelib.idle_test.htest import run
307    run(_replace_dialog)
308