(root)/
Python-3.11.7/
Lib/
idlelib/
parenmatch.py
       1  """ParenMatch -- for parenthesis matching.
       2  
       3  When you hit a right paren, the cursor should move briefly to the left
       4  paren.  Paren here is used generically; the matching applies to
       5  parentheses, square brackets, and curly braces.
       6  """
       7  from idlelib.hyperparser import HyperParser
       8  from idlelib.config import idleConf
       9  
      10  _openers = {')':'(',']':'[','}':'{'}
      11  CHECK_DELAY = 100 # milliseconds
      12  
      13  class ESC[4;38;5;81mParenMatch:
      14      """Highlight matching openers and closers, (), [], and {}.
      15  
      16      There are three supported styles of paren matching.  When a right
      17      paren (opener) is typed:
      18  
      19      opener -- highlight the matching left paren (closer);
      20      parens -- highlight the left and right parens (opener and closer);
      21      expression -- highlight the entire expression from opener to closer.
      22      (For back compatibility, 'default' is a synonym for 'opener').
      23  
      24      Flash-delay is the maximum milliseconds the highlighting remains.
      25      Any cursor movement (key press or click) before that removes the
      26      highlight.  If flash-delay is 0, there is no maximum.
      27  
      28      TODO:
      29      - Augment bell() with mismatch warning in status window.
      30      - Highlight when cursor is moved to the right of a closer.
      31        This might be too expensive to check.
      32      """
      33  
      34      RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
      35      # We want the restore event be called before the usual return and
      36      # backspace events.
      37      RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>",
      38                           "<Key-Return>", "<Key-BackSpace>")
      39  
      40      def __init__(self, editwin):
      41          self.editwin = editwin
      42          self.text = editwin.text
      43          # Bind the check-restore event to the function restore_event,
      44          # so that we can then use activate_restore (which calls event_add)
      45          # and deactivate_restore (which calls event_delete).
      46          editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME,
      47                            self.restore_event)
      48          self.counter = 0
      49          self.is_restore_active = 0
      50  
      51      @classmethod
      52      def reload(cls):
      53          cls.STYLE = idleConf.GetOption(
      54              'extensions','ParenMatch','style', default='opener')
      55          cls.FLASH_DELAY = idleConf.GetOption(
      56                  'extensions','ParenMatch','flash-delay', type='int',default=500)
      57          cls.BELL = idleConf.GetOption(
      58                  'extensions','ParenMatch','bell', type='bool', default=1)
      59          cls.HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),
      60                                                    'hilite')
      61  
      62      def activate_restore(self):
      63          "Activate mechanism to restore text from highlighting."
      64          if not self.is_restore_active:
      65              for seq in self.RESTORE_SEQUENCES:
      66                  self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
      67              self.is_restore_active = True
      68  
      69      def deactivate_restore(self):
      70          "Remove restore event bindings."
      71          if self.is_restore_active:
      72              for seq in self.RESTORE_SEQUENCES:
      73                  self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
      74              self.is_restore_active = False
      75  
      76      def flash_paren_event(self, event):
      77          "Handle editor 'show surrounding parens' event (menu or shortcut)."
      78          indices = (HyperParser(self.editwin, "insert")
      79                     .get_surrounding_brackets())
      80          self.finish_paren_event(indices)
      81          return "break"
      82  
      83      def paren_closed_event(self, event):
      84          "Handle user input of closer."
      85          # If user bound non-closer to <<paren-closed>>, quit.
      86          closer = self.text.get("insert-1c")
      87          if closer not in _openers:
      88              return
      89          hp = HyperParser(self.editwin, "insert-1c")
      90          if not hp.is_in_code():
      91              return
      92          indices = hp.get_surrounding_brackets(_openers[closer], True)
      93          self.finish_paren_event(indices)
      94          return  # Allow calltips to see ')'
      95  
      96      def finish_paren_event(self, indices):
      97          if indices is None and self.BELL:
      98              self.text.bell()
      99              return
     100          self.activate_restore()
     101          # self.create_tag(indices)
     102          self.tagfuncs.get(self.STYLE, self.create_tag_expression)(self, indices)
     103          # self.set_timeout()
     104          (self.set_timeout_last if self.FLASH_DELAY else
     105                              self.set_timeout_none)()
     106  
     107      def restore_event(self, event=None):
     108          "Remove effect of doing match."
     109          self.text.tag_delete("paren")
     110          self.deactivate_restore()
     111          self.counter += 1   # disable the last timer, if there is one.
     112  
     113      def handle_restore_timer(self, timer_count):
     114          if timer_count == self.counter:
     115              self.restore_event()
     116  
     117      # any one of the create_tag_XXX methods can be used depending on
     118      # the style
     119  
     120      def create_tag_opener(self, indices):
     121          """Highlight the single paren that matches"""
     122          self.text.tag_add("paren", indices[0])
     123          self.text.tag_config("paren", self.HILITE_CONFIG)
     124  
     125      def create_tag_parens(self, indices):
     126          """Highlight the left and right parens"""
     127          if self.text.get(indices[1]) in (')', ']', '}'):
     128              rightindex = indices[1]+"+1c"
     129          else:
     130              rightindex = indices[1]
     131          self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex)
     132          self.text.tag_config("paren", self.HILITE_CONFIG)
     133  
     134      def create_tag_expression(self, indices):
     135          """Highlight the entire expression"""
     136          if self.text.get(indices[1]) in (')', ']', '}'):
     137              rightindex = indices[1]+"+1c"
     138          else:
     139              rightindex = indices[1]
     140          self.text.tag_add("paren", indices[0], rightindex)
     141          self.text.tag_config("paren", self.HILITE_CONFIG)
     142  
     143      tagfuncs = {
     144          'opener': create_tag_opener,
     145          'default': create_tag_opener,
     146          'parens': create_tag_parens,
     147          'expression': create_tag_expression,
     148          }
     149  
     150      # any one of the set_timeout_XXX methods can be used depending on
     151      # the style
     152  
     153      def set_timeout_none(self):
     154          """Highlight will remain until user input turns it off
     155          or the insert has moved"""
     156          # After CHECK_DELAY, call a function which disables the "paren" tag
     157          # if the event is for the most recent timer and the insert has changed,
     158          # or schedules another call for itself.
     159          self.counter += 1
     160          def callme(callme, self=self, c=self.counter,
     161                     index=self.text.index("insert")):
     162              if index != self.text.index("insert"):
     163                  self.handle_restore_timer(c)
     164              else:
     165                  self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
     166          self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
     167  
     168      def set_timeout_last(self):
     169          """The last highlight created will be removed after FLASH_DELAY millisecs"""
     170          # associate a counter with an event; only disable the "paren"
     171          # tag if the event is for the most recent timer.
     172          self.counter += 1
     173          self.editwin.text_frame.after(
     174              self.FLASH_DELAY,
     175              lambda self=self, c=self.counter: self.handle_restore_timer(c))
     176  
     177  
     178  ParenMatch.reload()
     179  
     180  
     181  if __name__ == '__main__':
     182      from unittest import main
     183      main('idlelib.idle_test.test_parenmatch', verbosity=2)