• 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):
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)
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
53    def open(self, text):
54        """Make dialog visible on top of others and ready to use.
55
56        Also, highlight the currently selected text and set the
57        search to include the current selection (self.ok).
58
59        Args:
60            text: Text widget being searched.
61        """
62        SearchDialogBase.open(self, text)
63        try:
64            first = text.index("sel.first")
65        except TclError:
66            first = None
67        try:
68            last = text.index("sel.last")
69        except TclError:
70            last = None
71        first = first or text.index("insert")
72        last = last or first
73        self.show_hit(first, last)
74        self.ok = True
75
76    def create_entries(self):
77        "Create base and additional label and text entry widgets."
78        SearchDialogBase.create_entries(self)
79        self.replent = self.make_entry("Replace with:", self.replvar)[0]
80
81    def create_command_buttons(self):
82        """Create base and additional command buttons.
83
84        The additional buttons are for Find, Replace,
85        Replace+Find, and Replace All.
86        """
87        SearchDialogBase.create_command_buttons(self)
88        self.make_button("Find", self.find_it)
89        self.make_button("Replace", self.replace_it)
90        self.make_button("Replace+Find", self.default_command, isdef=True)
91        self.make_button("Replace All", self.replace_all)
92
93    def find_it(self, event=None):
94        "Handle the Find button."
95        self.do_find(False)
96
97    def replace_it(self, event=None):
98        """Handle the Replace button.
99
100        If the find is successful, then perform replace.
101        """
102        if self.do_find(self.ok):
103            self.do_replace()
104
105    def default_command(self, event=None):
106        """Handle the Replace+Find button as the default command.
107
108        First performs a replace and then, if the replace was
109        successful, a find next.
110        """
111        if self.do_find(self.ok):
112            if self.do_replace():  # Only find next match if replace succeeded.
113                                   # A bad re can cause it to fail.
114                self.do_find(False)
115
116    def _replace_expand(self, m, repl):
117        "Expand replacement text if regular expression."
118        if self.engine.isre():
119            try:
120                new = m.expand(repl)
121            except re.error:
122                self.engine.report_error(repl, 'Invalid Replace Expression')
123                new = None
124        else:
125            new = repl
126
127        return new
128
129    def replace_all(self, event=None):
130        """Handle the Replace All button.
131
132        Search text for occurrences of the Find value and replace
133        each of them.  The 'wrap around' value controls the start
134        point for searching.  If wrap isn't set, then the searching
135        starts at the first occurrence after the current selection;
136        if wrap is set, the replacement starts at the first line.
137        The replacement is always done top-to-bottom in the text.
138        """
139        prog = self.engine.getprog()
140        if not prog:
141            return
142        repl = self.replvar.get()
143        text = self.text
144        res = self.engine.search_text(text, prog)
145        if not res:
146            self.bell()
147            return
148        text.tag_remove("sel", "1.0", "end")
149        text.tag_remove("hit", "1.0", "end")
150        line = res[0]
151        col = res[1].start()
152        if self.engine.iswrap():
153            line = 1
154            col = 0
155        ok = True
156        first = last = None
157        # XXX ought to replace circular instead of top-to-bottom when wrapping
158        text.undo_block_start()
159        while True:
160            res = self.engine.search_forward(text, prog, line, col,
161                                             wrap=False, ok=ok)
162            if not res:
163                break
164            line, m = res
165            chars = text.get("%d.0" % line, "%d.0" % (line+1))
166            orig = m.group()
167            new = self._replace_expand(m, repl)
168            if new is None:
169                break
170            i, j = m.span()
171            first = "%d.%d" % (line, i)
172            last = "%d.%d" % (line, j)
173            if new == orig:
174                text.mark_set("insert", last)
175            else:
176                text.mark_set("insert", first)
177                if first != last:
178                    text.delete(first, last)
179                if new:
180                    text.insert(first, new)
181            col = i + len(new)
182            ok = False
183        text.undo_block_stop()
184        if first and last:
185            self.show_hit(first, last)
186        self.close()
187
188    def do_find(self, ok=False):
189        """Search for and highlight next occurrence of pattern in text.
190
191        No text replacement is done with this option.
192        """
193        if not self.engine.getprog():
194            return False
195        text = self.text
196        res = self.engine.search_text(text, None, ok)
197        if not res:
198            self.bell()
199            return False
200        line, m = res
201        i, j = m.span()
202        first = "%d.%d" % (line, i)
203        last = "%d.%d" % (line, j)
204        self.show_hit(first, last)
205        self.ok = True
206        return True
207
208    def do_replace(self):
209        "Replace search pattern in text with replacement value."
210        prog = self.engine.getprog()
211        if not prog:
212            return False
213        text = self.text
214        try:
215            first = pos = text.index("sel.first")
216            last = text.index("sel.last")
217        except TclError:
218            pos = None
219        if not pos:
220            first = last = pos = text.index("insert")
221        line, col = searchengine.get_line_col(pos)
222        chars = text.get("%d.0" % line, "%d.0" % (line+1))
223        m = prog.match(chars, col)
224        if not prog:
225            return False
226        new = self._replace_expand(m, self.replvar.get())
227        if new is None:
228            return False
229        text.mark_set("insert", first)
230        text.undo_block_start()
231        if m.group():
232            text.delete(first, last)
233        if new:
234            text.insert(first, new)
235        text.undo_block_stop()
236        self.show_hit(first, text.index("insert"))
237        self.ok = False
238        return True
239
240    def show_hit(self, first, last):
241        """Highlight text between first and last indices.
242
243        Text is highlighted via the 'hit' tag and the marked
244        section is brought into view.
245
246        The colors from the 'hit' tag aren't currently shown
247        when the text is displayed.  This is due to the 'sel'
248        tag being added first, so the colors in the 'sel'
249        config are seen instead of the colors for 'hit'.
250        """
251        text = self.text
252        text.mark_set("insert", first)
253        text.tag_remove("sel", "1.0", "end")
254        text.tag_add("sel", first, last)
255        text.tag_remove("hit", "1.0", "end")
256        if first == last:
257            text.tag_add("hit", first)
258        else:
259            text.tag_add("hit", first, last)
260        text.see("insert")
261        text.update_idletasks()
262
263    def close(self, event=None):
264        "Close the dialog and remove hit tags."
265        SearchDialogBase.close(self, event)
266        self.text.tag_remove("hit", "1.0", "end")
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