Home | History | Annotate | Download | only in idlelib
      1 """Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
      2 Uses idlelib.SearchEngine for search capability.
      3 Defines various replace related functions like replace, replace all,
      4 replace+find.
      5 """
      6 import re
      7 
      8 from tkinter import StringVar, TclError
      9 
     10 from idlelib.searchbase import SearchDialogBase
     11 from idlelib import searchengine
     12 
     13 def replace(text):
     14     """Returns a singleton ReplaceDialog instance.The single dialog
     15      saves user entries and preferences across instances."""
     16     root = text._root()
     17     engine = searchengine.get(root)
     18     if not hasattr(engine, "_replacedialog"):
     19         engine._replacedialog = ReplaceDialog(root, engine)
     20     dialog = engine._replacedialog
     21     dialog.open(text)
     22 
     23 
     24 class ReplaceDialog(SearchDialogBase):
     25 
     26     title = "Replace Dialog"
     27     icon = "Replace"
     28 
     29     def __init__(self, root, engine):
     30         SearchDialogBase.__init__(self, root, engine)
     31         self.replvar = StringVar(root)
     32 
     33     def open(self, text):
     34         """Display the replace dialog"""
     35         SearchDialogBase.open(self, text)
     36         try:
     37             first = text.index("sel.first")
     38         except TclError:
     39             first = None
     40         try:
     41             last = text.index("sel.last")
     42         except TclError:
     43             last = None
     44         first = first or text.index("insert")
     45         last = last or first
     46         self.show_hit(first, last)
     47         self.ok = 1
     48 
     49     def create_entries(self):
     50         """Create label and text entry widgets"""
     51         SearchDialogBase.create_entries(self)
     52         self.replent = self.make_entry("Replace with:", self.replvar)[0]
     53 
     54     def create_command_buttons(self):
     55         SearchDialogBase.create_command_buttons(self)
     56         self.make_button("Find", self.find_it)
     57         self.make_button("Replace", self.replace_it)
     58         self.make_button("Replace+Find", self.default_command, 1)
     59         self.make_button("Replace All", self.replace_all)
     60 
     61     def find_it(self, event=None):
     62         self.do_find(0)
     63 
     64     def replace_it(self, event=None):
     65         if self.do_find(self.ok):
     66             self.do_replace()
     67 
     68     def default_command(self, event=None):
     69         "Replace and find next."
     70         if self.do_find(self.ok):
     71             if self.do_replace():  # Only find next match if replace succeeded.
     72                                    # A bad re can cause it to fail.
     73                 self.do_find(0)
     74 
     75     def _replace_expand(self, m, repl):
     76         """ Helper function for expanding a regular expression
     77             in the replace field, if needed. """
     78         if self.engine.isre():
     79             try:
     80                 new = m.expand(repl)
     81             except re.error:
     82                 self.engine.report_error(repl, 'Invalid Replace Expression')
     83                 new = None
     84         else:
     85             new = repl
     86 
     87         return new
     88 
     89     def replace_all(self, event=None):
     90         """Replace all instances of patvar with replvar in text"""
     91         prog = self.engine.getprog()
     92         if not prog:
     93             return
     94         repl = self.replvar.get()
     95         text = self.text
     96         res = self.engine.search_text(text, prog)
     97         if not res:
     98             self.bell()
     99             return
    100         text.tag_remove("sel", "1.0", "end")
    101         text.tag_remove("hit", "1.0", "end")
    102         line = res[0]
    103         col = res[1].start()
    104         if self.engine.iswrap():
    105             line = 1
    106             col = 0
    107         ok = 1
    108         first = last = None
    109         # XXX ought to replace circular instead of top-to-bottom when wrapping
    110         text.undo_block_start()
    111         while 1:
    112             res = self.engine.search_forward(text, prog, line, col, 0, ok)
    113             if not res:
    114                 break
    115             line, m = res
    116             chars = text.get("%d.0" % line, "%d.0" % (line+1))
    117             orig = m.group()
    118             new = self._replace_expand(m, repl)
    119             if new is None:
    120                 break
    121             i, j = m.span()
    122             first = "%d.%d" % (line, i)
    123             last = "%d.%d" % (line, j)
    124             if new == orig:
    125                 text.mark_set("insert", last)
    126             else:
    127                 text.mark_set("insert", first)
    128                 if first != last:
    129                     text.delete(first, last)
    130                 if new:
    131                     text.insert(first, new)
    132             col = i + len(new)
    133             ok = 0
    134         text.undo_block_stop()
    135         if first and last:
    136             self.show_hit(first, last)
    137         self.close()
    138 
    139     def do_find(self, ok=0):
    140         if not self.engine.getprog():
    141             return False
    142         text = self.text
    143         res = self.engine.search_text(text, None, ok)
    144         if not res:
    145             self.bell()
    146             return False
    147         line, m = res
    148         i, j = m.span()
    149         first = "%d.%d" % (line, i)
    150         last = "%d.%d" % (line, j)
    151         self.show_hit(first, last)
    152         self.ok = 1
    153         return True
    154 
    155     def do_replace(self):
    156         prog = self.engine.getprog()
    157         if not prog:
    158             return False
    159         text = self.text
    160         try:
    161             first = pos = text.index("sel.first")
    162             last = text.index("sel.last")
    163         except TclError:
    164             pos = None
    165         if not pos:
    166             first = last = pos = text.index("insert")
    167         line, col = searchengine.get_line_col(pos)
    168         chars = text.get("%d.0" % line, "%d.0" % (line+1))
    169         m = prog.match(chars, col)
    170         if not prog:
    171             return False
    172         new = self._replace_expand(m, self.replvar.get())
    173         if new is None:
    174             return False
    175         text.mark_set("insert", first)
    176         text.undo_block_start()
    177         if m.group():
    178             text.delete(first, last)
    179         if new:
    180             text.insert(first, new)
    181         text.undo_block_stop()
    182         self.show_hit(first, text.index("insert"))
    183         self.ok = 0
    184         return True
    185 
    186     def show_hit(self, first, last):
    187         """Highlight text from 'first' to 'last'.
    188         'first', 'last' - Text indices"""
    189         text = self.text
    190         text.mark_set("insert", first)
    191         text.tag_remove("sel", "1.0", "end")
    192         text.tag_add("sel", first, last)
    193         text.tag_remove("hit", "1.0", "end")
    194         if first == last:
    195             text.tag_add("hit", first)
    196         else:
    197             text.tag_add("hit", first, last)
    198         text.see("insert")
    199         text.update_idletasks()
    200 
    201     def close(self, event=None):
    202         SearchDialogBase.close(self, event)
    203         self.text.tag_remove("hit", "1.0", "end")
    204 
    205 
    206 def _replace_dialog(parent):  # htest #
    207     from tkinter import Toplevel, Text, END, SEL
    208     from tkinter.ttk import Button
    209 
    210     box = Toplevel(parent)
    211     box.title("Test ReplaceDialog")
    212     x, y = map(int, parent.geometry().split('+')[1:])
    213     box.geometry("+%d+%d" % (x, y + 175))
    214 
    215     # mock undo delegator methods
    216     def undo_block_start():
    217         pass
    218 
    219     def undo_block_stop():
    220         pass
    221 
    222     text = Text(box, inactiveselectbackground='gray')
    223     text.undo_block_start = undo_block_start
    224     text.undo_block_stop = undo_block_stop
    225     text.pack()
    226     text.insert("insert","This is a sample sTring\nPlus MORE.")
    227     text.focus_set()
    228 
    229     def show_replace():
    230         text.tag_add(SEL, "1.0", END)
    231         replace(text)
    232         text.tag_remove(SEL, "1.0", END)
    233 
    234     button = Button(box, text="Replace", command=show_replace)
    235     button.pack()
    236 
    237 if __name__ == '__main__':
    238     import unittest
    239     unittest.main('idlelib.idle_test.test_replace',
    240                 verbosity=2, exit=False)
    241 
    242     from idlelib.idle_test.htest import run
    243     run(_replace_dialog)
    244