jpayne@69: """Replace dialog for IDLE. Inherits SearchDialogBase for GUI. jpayne@69: Uses idlelib.searchengine.SearchEngine for search capability. jpayne@69: Defines various replace related functions like replace, replace all, jpayne@69: and replace+find. jpayne@69: """ jpayne@69: import re jpayne@69: jpayne@69: from tkinter import StringVar, TclError jpayne@69: jpayne@69: from idlelib.searchbase import SearchDialogBase jpayne@69: from idlelib import searchengine jpayne@69: jpayne@69: jpayne@69: def replace(text): jpayne@69: """Create or reuse a singleton ReplaceDialog instance. jpayne@69: jpayne@69: The singleton dialog saves user entries and preferences jpayne@69: across instances. jpayne@69: jpayne@69: Args: jpayne@69: text: Text widget containing the text to be searched. jpayne@69: """ jpayne@69: root = text._root() jpayne@69: engine = searchengine.get(root) jpayne@69: if not hasattr(engine, "_replacedialog"): jpayne@69: engine._replacedialog = ReplaceDialog(root, engine) jpayne@69: dialog = engine._replacedialog jpayne@69: dialog.open(text) jpayne@69: jpayne@69: jpayne@69: class ReplaceDialog(SearchDialogBase): jpayne@69: "Dialog for finding and replacing a pattern in text." jpayne@69: jpayne@69: title = "Replace Dialog" jpayne@69: icon = "Replace" jpayne@69: jpayne@69: def __init__(self, root, engine): jpayne@69: """Create search dialog for finding and replacing text. jpayne@69: jpayne@69: Uses SearchDialogBase as the basis for the GUI and a jpayne@69: searchengine instance to prepare the search. jpayne@69: jpayne@69: Attributes: jpayne@69: replvar: StringVar containing 'Replace with:' value. jpayne@69: replent: Entry widget for replvar. Created in jpayne@69: create_entries(). jpayne@69: ok: Boolean used in searchengine.search_text to indicate jpayne@69: whether the search includes the selection. jpayne@69: """ jpayne@69: super().__init__(root, engine) jpayne@69: self.replvar = StringVar(root) jpayne@69: jpayne@69: def open(self, text): jpayne@69: """Make dialog visible on top of others and ready to use. jpayne@69: jpayne@69: Also, highlight the currently selected text and set the jpayne@69: search to include the current selection (self.ok). jpayne@69: jpayne@69: Args: jpayne@69: text: Text widget being searched. jpayne@69: """ jpayne@69: SearchDialogBase.open(self, text) jpayne@69: try: jpayne@69: first = text.index("sel.first") jpayne@69: except TclError: jpayne@69: first = None jpayne@69: try: jpayne@69: last = text.index("sel.last") jpayne@69: except TclError: jpayne@69: last = None jpayne@69: first = first or text.index("insert") jpayne@69: last = last or first jpayne@69: self.show_hit(first, last) jpayne@69: self.ok = True jpayne@69: jpayne@69: def create_entries(self): jpayne@69: "Create base and additional label and text entry widgets." jpayne@69: SearchDialogBase.create_entries(self) jpayne@69: self.replent = self.make_entry("Replace with:", self.replvar)[0] jpayne@69: jpayne@69: def create_command_buttons(self): jpayne@69: """Create base and additional command buttons. jpayne@69: jpayne@69: The additional buttons are for Find, Replace, jpayne@69: Replace+Find, and Replace All. jpayne@69: """ jpayne@69: SearchDialogBase.create_command_buttons(self) jpayne@69: self.make_button("Find", self.find_it) jpayne@69: self.make_button("Replace", self.replace_it) jpayne@69: self.make_button("Replace+Find", self.default_command, isdef=True) jpayne@69: self.make_button("Replace All", self.replace_all) jpayne@69: jpayne@69: def find_it(self, event=None): jpayne@69: "Handle the Find button." jpayne@69: self.do_find(False) jpayne@69: jpayne@69: def replace_it(self, event=None): jpayne@69: """Handle the Replace button. jpayne@69: jpayne@69: If the find is successful, then perform replace. jpayne@69: """ jpayne@69: if self.do_find(self.ok): jpayne@69: self.do_replace() jpayne@69: jpayne@69: def default_command(self, event=None): jpayne@69: """Handle the Replace+Find button as the default command. jpayne@69: jpayne@69: First performs a replace and then, if the replace was jpayne@69: successful, a find next. jpayne@69: """ jpayne@69: if self.do_find(self.ok): jpayne@69: if self.do_replace(): # Only find next match if replace succeeded. jpayne@69: # A bad re can cause it to fail. jpayne@69: self.do_find(False) jpayne@69: jpayne@69: def _replace_expand(self, m, repl): jpayne@69: "Expand replacement text if regular expression." jpayne@69: if self.engine.isre(): jpayne@69: try: jpayne@69: new = m.expand(repl) jpayne@69: except re.error: jpayne@69: self.engine.report_error(repl, 'Invalid Replace Expression') jpayne@69: new = None jpayne@69: else: jpayne@69: new = repl jpayne@69: jpayne@69: return new jpayne@69: jpayne@69: def replace_all(self, event=None): jpayne@69: """Handle the Replace All button. jpayne@69: jpayne@69: Search text for occurrences of the Find value and replace jpayne@69: each of them. The 'wrap around' value controls the start jpayne@69: point for searching. If wrap isn't set, then the searching jpayne@69: starts at the first occurrence after the current selection; jpayne@69: if wrap is set, the replacement starts at the first line. jpayne@69: The replacement is always done top-to-bottom in the text. jpayne@69: """ jpayne@69: prog = self.engine.getprog() jpayne@69: if not prog: jpayne@69: return jpayne@69: repl = self.replvar.get() jpayne@69: text = self.text jpayne@69: res = self.engine.search_text(text, prog) jpayne@69: if not res: jpayne@69: self.bell() jpayne@69: return jpayne@69: text.tag_remove("sel", "1.0", "end") jpayne@69: text.tag_remove("hit", "1.0", "end") jpayne@69: line = res[0] jpayne@69: col = res[1].start() jpayne@69: if self.engine.iswrap(): jpayne@69: line = 1 jpayne@69: col = 0 jpayne@69: ok = True jpayne@69: first = last = None jpayne@69: # XXX ought to replace circular instead of top-to-bottom when wrapping jpayne@69: text.undo_block_start() jpayne@69: while True: jpayne@69: res = self.engine.search_forward(text, prog, line, col, jpayne@69: wrap=False, ok=ok) jpayne@69: if not res: jpayne@69: break jpayne@69: line, m = res jpayne@69: chars = text.get("%d.0" % line, "%d.0" % (line+1)) jpayne@69: orig = m.group() jpayne@69: new = self._replace_expand(m, repl) jpayne@69: if new is None: jpayne@69: break jpayne@69: i, j = m.span() jpayne@69: first = "%d.%d" % (line, i) jpayne@69: last = "%d.%d" % (line, j) jpayne@69: if new == orig: jpayne@69: text.mark_set("insert", last) jpayne@69: else: jpayne@69: text.mark_set("insert", first) jpayne@69: if first != last: jpayne@69: text.delete(first, last) jpayne@69: if new: jpayne@69: text.insert(first, new) jpayne@69: col = i + len(new) jpayne@69: ok = False jpayne@69: text.undo_block_stop() jpayne@69: if first and last: jpayne@69: self.show_hit(first, last) jpayne@69: self.close() jpayne@69: jpayne@69: def do_find(self, ok=False): jpayne@69: """Search for and highlight next occurrence of pattern in text. jpayne@69: jpayne@69: No text replacement is done with this option. jpayne@69: """ jpayne@69: if not self.engine.getprog(): jpayne@69: return False jpayne@69: text = self.text jpayne@69: res = self.engine.search_text(text, None, ok) jpayne@69: if not res: jpayne@69: self.bell() jpayne@69: return False jpayne@69: line, m = res jpayne@69: i, j = m.span() jpayne@69: first = "%d.%d" % (line, i) jpayne@69: last = "%d.%d" % (line, j) jpayne@69: self.show_hit(first, last) jpayne@69: self.ok = True jpayne@69: return True jpayne@69: jpayne@69: def do_replace(self): jpayne@69: "Replace search pattern in text with replacement value." jpayne@69: prog = self.engine.getprog() jpayne@69: if not prog: jpayne@69: return False jpayne@69: text = self.text jpayne@69: try: jpayne@69: first = pos = text.index("sel.first") jpayne@69: last = text.index("sel.last") jpayne@69: except TclError: jpayne@69: pos = None jpayne@69: if not pos: jpayne@69: first = last = pos = text.index("insert") jpayne@69: line, col = searchengine.get_line_col(pos) jpayne@69: chars = text.get("%d.0" % line, "%d.0" % (line+1)) jpayne@69: m = prog.match(chars, col) jpayne@69: if not prog: jpayne@69: return False jpayne@69: new = self._replace_expand(m, self.replvar.get()) jpayne@69: if new is None: jpayne@69: return False jpayne@69: text.mark_set("insert", first) jpayne@69: text.undo_block_start() jpayne@69: if m.group(): jpayne@69: text.delete(first, last) jpayne@69: if new: jpayne@69: text.insert(first, new) jpayne@69: text.undo_block_stop() jpayne@69: self.show_hit(first, text.index("insert")) jpayne@69: self.ok = False jpayne@69: return True jpayne@69: jpayne@69: def show_hit(self, first, last): jpayne@69: """Highlight text between first and last indices. jpayne@69: jpayne@69: Text is highlighted via the 'hit' tag and the marked jpayne@69: section is brought into view. jpayne@69: jpayne@69: The colors from the 'hit' tag aren't currently shown jpayne@69: when the text is displayed. This is due to the 'sel' jpayne@69: tag being added first, so the colors in the 'sel' jpayne@69: config are seen instead of the colors for 'hit'. jpayne@69: """ jpayne@69: text = self.text jpayne@69: text.mark_set("insert", first) jpayne@69: text.tag_remove("sel", "1.0", "end") jpayne@69: text.tag_add("sel", first, last) jpayne@69: text.tag_remove("hit", "1.0", "end") jpayne@69: if first == last: jpayne@69: text.tag_add("hit", first) jpayne@69: else: jpayne@69: text.tag_add("hit", first, last) jpayne@69: text.see("insert") jpayne@69: text.update_idletasks() jpayne@69: jpayne@69: def close(self, event=None): jpayne@69: "Close the dialog and remove hit tags." jpayne@69: SearchDialogBase.close(self, event) jpayne@69: self.text.tag_remove("hit", "1.0", "end") jpayne@69: jpayne@69: jpayne@69: def _replace_dialog(parent): # htest # jpayne@69: from tkinter import Toplevel, Text, END, SEL jpayne@69: from tkinter.ttk import Frame, Button jpayne@69: jpayne@69: top = Toplevel(parent) jpayne@69: top.title("Test ReplaceDialog") jpayne@69: x, y = map(int, parent.geometry().split('+')[1:]) jpayne@69: top.geometry("+%d+%d" % (x, y + 175)) jpayne@69: jpayne@69: # mock undo delegator methods jpayne@69: def undo_block_start(): jpayne@69: pass jpayne@69: jpayne@69: def undo_block_stop(): jpayne@69: pass jpayne@69: jpayne@69: frame = Frame(top) jpayne@69: frame.pack() jpayne@69: text = Text(frame, inactiveselectbackground='gray') jpayne@69: text.undo_block_start = undo_block_start jpayne@69: text.undo_block_stop = undo_block_stop jpayne@69: text.pack() jpayne@69: text.insert("insert","This is a sample sTring\nPlus MORE.") jpayne@69: text.focus_set() jpayne@69: jpayne@69: def show_replace(): jpayne@69: text.tag_add(SEL, "1.0", END) jpayne@69: replace(text) jpayne@69: text.tag_remove(SEL, "1.0", END) jpayne@69: jpayne@69: button = Button(frame, text="Replace", command=show_replace) jpayne@69: button.pack() jpayne@69: jpayne@69: if __name__ == '__main__': jpayne@69: from unittest import main jpayne@69: main('idlelib.idle_test.test_replace', verbosity=2, exit=False) jpayne@69: jpayne@69: from idlelib.idle_test.htest import run jpayne@69: run(_replace_dialog)