mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 11:49:12 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			307 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""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):
 | 
						|
    """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)
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
 | 
						|
    def open(self, text):
 | 
						|
        """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
 | 
						|
 | 
						|
    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 True:
 | 
						|
            res = self.engine.search_forward(text, prog, line, col,
 | 
						|
                                             wrap=False, ok=ok)
 | 
						|
            if not res:
 | 
						|
                break
 | 
						|
            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)
 | 
						|
            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)
 | 
						|
        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")
 | 
						|
 | 
						|
 | 
						|
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)
 |