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 replacefind 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 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 createentries ok boolean used in searchengine searchtext to indicate whether the search includes the selection 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 create base and additional command buttons the additional buttons are for find replace replacefind and replace all handle the replace button if the find is successful then perform replace handle the replacefind button as the default command first performs a replace and then if the replace was successful a find next a bad re can cause it to fail 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 toptobottom in the text xxx ought to replace circular instead of toptobottom when wrapping search for and highlight next occurrence of pattern in text no text replacement is done with this option 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 mock undo delegator methods 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 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 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 create base and additional command buttons the additional buttons are for find replace replace find and replace all handle the replace button if the find is successful then perform replace handle the replace find button as the default command first performs a replace and then if the replace was successful a find next only find next match if replace succeeded a bad re can cause it to fail 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 xxx ought to replace circular instead of top to bottom when wrapping search for and highlight next occurrence of pattern in text no text replacement is done with this option 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 htest mock undo delegator methods
import re

from tkinter import StringVar, TclError

from idlelib.searchbase import SearchDialogBase
from idlelib import searchengine


def replace(text, insert_tags=None):
    
    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):
        
        super().__init__(root, engine)
        self.replvar = StringVar(root)
        self.insert_tags = None

    def open(self, text, insert_tags=None):
        
        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):
        
        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):
        
        if self.do_find(self.ok):
            self.do_replace()

    def default_command(self, event=None):
        
        if self.do_find(self.ok):
            if self.do_replace():  
                                   
                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):
        
        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
        
        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):
        
        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):
        
        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):  
    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))

    
    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)