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