• 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 True:
162            res = self.engine.search_forward(text, prog, line, col,
163                                             wrap=False, ok=ok)
164            if not res:
165                break
166            line, m = res
167            chars = text.get("%d.0" % line, "%d.0" % (line+1))
168            orig = m.group()
169            new = self._replace_expand(m, repl)
170            if new is None:
171                break
172            i, j = m.span()
173            first = "%d.%d" % (line, i)
174            last = "%d.%d" % (line, j)
175            if new == orig:
176                text.mark_set("insert", last)
177            else:
178                text.mark_set("insert", first)
179                if first != last:
180                    text.delete(first, last)
181                if new:
182                    text.insert(first, new, self.insert_tags)
183            col = i + len(new)
184            ok = False
185        text.undo_block_stop()
186        if first and last:
187            self.show_hit(first, last)
188        self.close()
189
190    def do_find(self, ok=False):
191        """Search for and highlight next occurrence of pattern in text.
192
193        No text replacement is done with this option.
194        """
195        if not self.engine.getprog():
196            return False
197        text = self.text
198        res = self.engine.search_text(text, None, ok)
199        if not res:
200            self.bell()
201            return False
202        line, m = res
203        i, j = m.span()
204        first = "%d.%d" % (line, i)
205        last = "%d.%d" % (line, j)
206        self.show_hit(first, last)
207        self.ok = True
208        return True
209
210    def do_replace(self):
211        "Replace search pattern in text with replacement value."
212        prog = self.engine.getprog()
213        if not prog:
214            return False
215        text = self.text
216        try:
217            first = pos = text.index("sel.first")
218            last = text.index("sel.last")
219        except TclError:
220            pos = None
221        if not pos:
222            first = last = pos = text.index("insert")
223        line, col = searchengine.get_line_col(pos)
224        chars = text.get("%d.0" % line, "%d.0" % (line+1))
225        m = prog.match(chars, col)
226        if not prog:
227            return False
228        new = self._replace_expand(m, self.replvar.get())
229        if new is None:
230            return False
231        text.mark_set("insert", first)
232        text.undo_block_start()
233        if m.group():
234            text.delete(first, last)
235        if new:
236            text.insert(first, new, self.insert_tags)
237        text.undo_block_stop()
238        self.show_hit(first, text.index("insert"))
239        self.ok = False
240        return True
241
242    def show_hit(self, first, last):
243        """Highlight text between first and last indices.
244
245        Text is highlighted via the 'hit' tag and the marked
246        section is brought into view.
247
248        The colors from the 'hit' tag aren't currently shown
249        when the text is displayed.  This is due to the 'sel'
250        tag being added first, so the colors in the 'sel'
251        config are seen instead of the colors for 'hit'.
252        """
253        text = self.text
254        text.mark_set("insert", first)
255        text.tag_remove("sel", "1.0", "end")
256        text.tag_add("sel", first, last)
257        text.tag_remove("hit", "1.0", "end")
258        if first == last:
259            text.tag_add("hit", first)
260        else:
261            text.tag_add("hit", first, last)
262        text.see("insert")
263        text.update_idletasks()
264
265    def close(self, event=None):
266        "Close the dialog and remove hit tags."
267        SearchDialogBase.close(self, event)
268        self.text.tag_remove("hit", "1.0", "end")
269        self.insert_tags = None
270
271
272def _replace_dialog(parent):  # htest #
273    from tkinter import Toplevel, Text, END, SEL
274    from tkinter.ttk import Frame, Button
275
276    top = Toplevel(parent)
277    top.title("Test ReplaceDialog")
278    x, y = map(int, parent.geometry().split('+')[1:])
279    top.geometry("+%d+%d" % (x, y + 175))
280
281    # mock undo delegator methods
282    def undo_block_start():
283        pass
284
285    def undo_block_stop():
286        pass
287
288    frame = Frame(top)
289    frame.pack()
290    text = Text(frame, inactiveselectbackground='gray')
291    text.undo_block_start = undo_block_start
292    text.undo_block_stop = undo_block_stop
293    text.pack()
294    text.insert("insert","This is a sample sTring\nPlus MORE.")
295    text.focus_set()
296
297    def show_replace():
298        text.tag_add(SEL, "1.0", END)
299        replace(text)
300        text.tag_remove(SEL, "1.0", END)
301
302    button = Button(frame, text="Replace", command=show_replace)
303    button.pack()
304
305if __name__ == '__main__':
306    from unittest import main
307    main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
308
309    from idlelib.idle_test.htest import run
310    run(_replace_dialog)
311