(root)/
Python-3.12.0/
Lib/
idlelib/
replace.py
       1  """Replace dialog for IDLE. Inherits SearchDialogBase for GUI.
       2  Uses idlelib.searchengine.SearchEngine for search capability.
       3  Defines various replace related functions like replace, replace all,
       4  and 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  
      14  def replace(text, insert_tags=None):
      15      """Create or reuse a singleton ReplaceDialog instance.
      16  
      17      The singleton dialog saves user entries and preferences
      18      across instances.
      19  
      20      Args:
      21          text: Text widget containing the text to be searched.
      22      """
      23      root = text._root()
      24      engine = searchengine.get(root)
      25      if not hasattr(engine, "_replacedialog"):
      26          engine._replacedialog = ReplaceDialog(root, engine)
      27      dialog = engine._replacedialog
      28      dialog.open(text, insert_tags=insert_tags)
      29  
      30  
      31  class ESC[4;38;5;81mReplaceDialog(ESC[4;38;5;149mSearchDialogBase):
      32      "Dialog for finding and replacing a pattern in text."
      33  
      34      title = "Replace Dialog"
      35      icon = "Replace"
      36  
      37      def __init__(self, root, engine):
      38          """Create search dialog for finding and replacing text.
      39  
      40          Uses SearchDialogBase as the basis for the GUI and a
      41          searchengine instance to prepare the search.
      42  
      43          Attributes:
      44              replvar: StringVar containing 'Replace with:' value.
      45              replent: Entry widget for replvar.  Created in
      46                  create_entries().
      47              ok: Boolean used in searchengine.search_text to indicate
      48                  whether the search includes the selection.
      49          """
      50          super().__init__(root, engine)
      51          self.replvar = StringVar(root)
      52          self.insert_tags = None
      53  
      54      def open(self, text, insert_tags=None):
      55          """Make dialog visible on top of others and ready to use.
      56  
      57          Also, highlight the currently selected text and set the
      58          search to include the current selection (self.ok).
      59  
      60          Args:
      61              text: Text widget being searched.
      62          """
      63          SearchDialogBase.open(self, text)
      64          try:
      65              first = text.index("sel.first")
      66          except TclError:
      67              first = None
      68          try:
      69              last = text.index("sel.last")
      70          except TclError:
      71              last = None
      72          first = first or text.index("insert")
      73          last = last or first
      74          self.show_hit(first, last)
      75          self.ok = True
      76          self.insert_tags = insert_tags
      77  
      78      def create_entries(self):
      79          "Create base and additional label and text entry widgets."
      80          SearchDialogBase.create_entries(self)
      81          self.replent = self.make_entry("Replace with:", self.replvar)[0]
      82  
      83      def create_command_buttons(self):
      84          """Create base and additional command buttons.
      85  
      86          The additional buttons are for Find, Replace,
      87          Replace+Find, and Replace All.
      88          """
      89          SearchDialogBase.create_command_buttons(self)
      90          self.make_button("Find", self.find_it)
      91          self.make_button("Replace", self.replace_it)
      92          self.make_button("Replace+Find", self.default_command, isdef=True)
      93          self.make_button("Replace All", self.replace_all)
      94  
      95      def find_it(self, event=None):
      96          "Handle the Find button."
      97          self.do_find(False)
      98  
      99      def replace_it(self, event=None):
     100          """Handle the Replace button.
     101  
     102          If the find is successful, then perform replace.
     103          """
     104          if self.do_find(self.ok):
     105              self.do_replace()
     106  
     107      def default_command(self, event=None):
     108          """Handle the Replace+Find button as the default command.
     109  
     110          First performs a replace and then, if the replace was
     111          successful, a find next.
     112          """
     113          if self.do_find(self.ok):
     114              if self.do_replace():  # Only find next match if replace succeeded.
     115                                     # A bad re can cause it to fail.
     116                  self.do_find(False)
     117  
     118      def _replace_expand(self, m, repl):
     119          "Expand replacement text if regular expression."
     120          if self.engine.isre():
     121              try:
     122                  new = m.expand(repl)
     123              except re.error:
     124                  self.engine.report_error(repl, 'Invalid Replace Expression')
     125                  new = None
     126          else:
     127              new = repl
     128  
     129          return new
     130  
     131      def replace_all(self, event=None):
     132          """Handle the Replace All button.
     133  
     134          Search text for occurrences of the Find value and replace
     135          each of them.  The 'wrap around' value controls the start
     136          point for searching.  If wrap isn't set, then the searching
     137          starts at the first occurrence after the current selection;
     138          if wrap is set, the replacement starts at the first line.
     139          The replacement is always done top-to-bottom in the text.
     140          """
     141          prog = self.engine.getprog()
     142          if not prog:
     143              return
     144          repl = self.replvar.get()
     145          text = self.text
     146          res = self.engine.search_text(text, prog)
     147          if not res:
     148              self.bell()
     149              return
     150          text.tag_remove("sel", "1.0", "end")
     151          text.tag_remove("hit", "1.0", "end")
     152          line = res[0]
     153          col = res[1].start()
     154          if self.engine.iswrap():
     155              line = 1
     156              col = 0
     157          ok = True
     158          first = last = None
     159          # XXX ought to replace circular instead of top-to-bottom when wrapping
     160          text.undo_block_start()
     161          while res := self.engine.search_forward(
     162                  text, prog, line, col, wrap=False, ok=ok):
     163              line, m = res
     164              chars = text.get("%d.0" % line, "%d.0" % (line+1))
     165              orig = m.group()
     166              new = self._replace_expand(m, repl)
     167              if new is None:
     168                  break
     169              i, j = m.span()
     170              first = "%d.%d" % (line, i)
     171              last = "%d.%d" % (line, j)
     172              if new == orig:
     173                  text.mark_set("insert", last)
     174              else:
     175                  text.mark_set("insert", first)
     176                  if first != last:
     177                      text.delete(first, last)
     178                  if new:
     179                      text.insert(first, new, self.insert_tags)
     180              col = i + len(new)
     181              ok = False
     182          text.undo_block_stop()
     183          if first and last:
     184              self.show_hit(first, last)
     185          self.close()
     186  
     187      def do_find(self, ok=False):
     188          """Search for and highlight next occurrence of pattern in text.
     189  
     190          No text replacement is done with this option.
     191          """
     192          if not self.engine.getprog():
     193              return False
     194          text = self.text
     195          res = self.engine.search_text(text, None, ok)
     196          if not res:
     197              self.bell()
     198              return False
     199          line, m = res
     200          i, j = m.span()
     201          first = "%d.%d" % (line, i)
     202          last = "%d.%d" % (line, j)
     203          self.show_hit(first, last)
     204          self.ok = True
     205          return True
     206  
     207      def do_replace(self):
     208          "Replace search pattern in text with replacement value."
     209          prog = self.engine.getprog()
     210          if not prog:
     211              return False
     212          text = self.text
     213          try:
     214              first = pos = text.index("sel.first")
     215              last = text.index("sel.last")
     216          except TclError:
     217              pos = None
     218          if not pos:
     219              first = last = pos = text.index("insert")
     220          line, col = searchengine.get_line_col(pos)
     221          chars = text.get("%d.0" % line, "%d.0" % (line+1))
     222          m = prog.match(chars, col)
     223          if not prog:
     224              return False
     225          new = self._replace_expand(m, self.replvar.get())
     226          if new is None:
     227              return False
     228          text.mark_set("insert", first)
     229          text.undo_block_start()
     230          if m.group():
     231              text.delete(first, last)
     232          if new:
     233              text.insert(first, new, self.insert_tags)
     234          text.undo_block_stop()
     235          self.show_hit(first, text.index("insert"))
     236          self.ok = False
     237          return True
     238  
     239      def show_hit(self, first, last):
     240          """Highlight text between first and last indices.
     241  
     242          Text is highlighted via the 'hit' tag and the marked
     243          section is brought into view.
     244  
     245          The colors from the 'hit' tag aren't currently shown
     246          when the text is displayed.  This is due to the 'sel'
     247          tag being added first, so the colors in the 'sel'
     248          config are seen instead of the colors for 'hit'.
     249          """
     250          text = self.text
     251          text.mark_set("insert", first)
     252          text.tag_remove("sel", "1.0", "end")
     253          text.tag_add("sel", first, last)
     254          text.tag_remove("hit", "1.0", "end")
     255          if first == last:
     256              text.tag_add("hit", first)
     257          else:
     258              text.tag_add("hit", first, last)
     259          text.see("insert")
     260          text.update_idletasks()
     261  
     262      def close(self, event=None):
     263          "Close the dialog and remove hit tags."
     264          SearchDialogBase.close(self, event)
     265          self.text.tag_remove("hit", "1.0", "end")
     266          self.insert_tags = None
     267  
     268  
     269  def _replace_dialog(parent):  # htest #
     270      from tkinter import Toplevel, Text, END, SEL
     271      from tkinter.ttk import Frame, Button
     272  
     273      top = Toplevel(parent)
     274      top.title("Test ReplaceDialog")
     275      x, y = map(int, parent.geometry().split('+')[1:])
     276      top.geometry("+%d+%d" % (x, y + 175))
     277  
     278      # mock undo delegator methods
     279      def undo_block_start():
     280          pass
     281  
     282      def undo_block_stop():
     283          pass
     284  
     285      frame = Frame(top)
     286      frame.pack()
     287      text = Text(frame, inactiveselectbackground='gray')
     288      text.undo_block_start = undo_block_start
     289      text.undo_block_stop = undo_block_stop
     290      text.pack()
     291      text.insert("insert","This is a sample sTring\nPlus MORE.")
     292      text.focus_set()
     293  
     294      def show_replace():
     295          text.tag_add(SEL, "1.0", END)
     296          replace(text)
     297          text.tag_remove(SEL, "1.0", END)
     298  
     299      button = Button(frame, text="Replace", command=show_replace)
     300      button.pack()
     301  
     302  if __name__ == '__main__':
     303      from unittest import main
     304      main('idlelib.idle_test.test_replace', verbosity=2, exit=False)
     305  
     306      from idlelib.idle_test.htest import run
     307      run(_replace_dialog)