Home | History | Annotate | Download | only in idlelib
      1 """ParenMatch -- An IDLE extension 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 ParenMatch:
     14     """Highlight matching parentheses
     15 
     16     There are three supported style of paren matching, based loosely
     17     on the Emacs options.  The style is select based on the
     18     HILITE_STYLE attribute; it can be changed used the set_style
     19     method.
     20 
     21     The supported styles are:
     22 
     23     default -- When a right paren is typed, highlight the matching
     24         left paren for 1/2 sec.
     25 
     26     expression -- When a right paren is typed, highlight the entire
     27         expression from the left paren to the right paren.
     28 
     29     TODO:
     30         - extend IDLE with configuration dialog to change options
     31         - implement rest of Emacs highlight styles (see below)
     32         - print mismatch warning in IDLE status window
     33 
     34     Note: In Emacs, there are several styles of highlight where the
     35     matching paren is highlighted whenever the cursor is immediately
     36     to the right of a right paren.  I don't know how to do that in Tk,
     37     so I haven't bothered.
     38     """
     39     menudefs = [
     40         ('edit', [
     41             ("Show surrounding parens", "<<flash-paren>>"),
     42         ])
     43     ]
     44     STYLE = idleConf.GetOption('extensions','ParenMatch','style',
     45             default='expression')
     46     FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
     47             type='int',default=500)
     48     HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
     49     BELL = idleConf.GetOption('extensions','ParenMatch','bell',
     50             type='bool',default=1)
     51 
     52     RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
     53     # We want the restore event be called before the usual return and
     54     # backspace events.
     55     RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>",
     56                          "<Key-Return>", "<Key-BackSpace>")
     57 
     58     def __init__(self, editwin):
     59         self.editwin = editwin
     60         self.text = editwin.text
     61         # Bind the check-restore event to the function restore_event,
     62         # so that we can then use activate_restore (which calls event_add)
     63         # and deactivate_restore (which calls event_delete).
     64         editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME,
     65                           self.restore_event)
     66         self.bell = self.text.bell if self.BELL else lambda: None
     67         self.counter = 0
     68         self.is_restore_active = 0
     69         self.set_style(self.STYLE)
     70 
     71     def activate_restore(self):
     72         if not self.is_restore_active:
     73             for seq in self.RESTORE_SEQUENCES:
     74                 self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
     75             self.is_restore_active = True
     76 
     77     def deactivate_restore(self):
     78         if self.is_restore_active:
     79             for seq in self.RESTORE_SEQUENCES:
     80                 self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
     81             self.is_restore_active = False
     82 
     83     def set_style(self, style):
     84         self.STYLE = style
     85         if style == "default":
     86             self.create_tag = self.create_tag_default
     87             self.set_timeout = self.set_timeout_last
     88         elif style == "expression":
     89             self.create_tag = self.create_tag_expression
     90             self.set_timeout = self.set_timeout_none
     91 
     92     def flash_paren_event(self, event):
     93         indices = (HyperParser(self.editwin, "insert")
     94                    .get_surrounding_brackets())
     95         if indices is None:
     96             self.bell()
     97             return
     98         self.activate_restore()
     99         self.create_tag(indices)
    100         self.set_timeout_last()
    101 
    102     def paren_closed_event(self, event):
    103         # If it was a shortcut and not really a closing paren, quit.
    104         closer = self.text.get("insert-1c")
    105         if closer not in _openers:
    106             return
    107         hp = HyperParser(self.editwin, "insert-1c")
    108         if not hp.is_in_code():
    109             return
    110         indices = hp.get_surrounding_brackets(_openers[closer], True)
    111         if indices is None:
    112             self.bell()
    113             return
    114         self.activate_restore()
    115         self.create_tag(indices)
    116         self.set_timeout()
    117 
    118     def restore_event(self, event=None):
    119         self.text.tag_delete("paren")
    120         self.deactivate_restore()
    121         self.counter += 1   # disable the last timer, if there is one.
    122 
    123     def handle_restore_timer(self, timer_count):
    124         if timer_count == self.counter:
    125             self.restore_event()
    126 
    127     # any one of the create_tag_XXX methods can be used depending on
    128     # the style
    129 
    130     def create_tag_default(self, indices):
    131         """Highlight the single paren that matches"""
    132         self.text.tag_add("paren", indices[0])
    133         self.text.tag_config("paren", self.HILITE_CONFIG)
    134 
    135     def create_tag_expression(self, indices):
    136         """Highlight the entire expression"""
    137         if self.text.get(indices[1]) in (')', ']', '}'):
    138             rightindex = indices[1]+"+1c"
    139         else:
    140             rightindex = indices[1]
    141         self.text.tag_add("paren", indices[0], rightindex)
    142         self.text.tag_config("paren", self.HILITE_CONFIG)
    143 
    144     # any one of the set_timeout_XXX methods can be used depending on
    145     # the style
    146 
    147     def set_timeout_none(self):
    148         """Highlight will remain until user input turns it off
    149         or the insert has moved"""
    150         # After CHECK_DELAY, call a function which disables the "paren" tag
    151         # if the event is for the most recent timer and the insert has changed,
    152         # or schedules another call for itself.
    153         self.counter += 1
    154         def callme(callme, self=self, c=self.counter,
    155                    index=self.text.index("insert")):
    156             if index != self.text.index("insert"):
    157                 self.handle_restore_timer(c)
    158             else:
    159                 self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
    160         self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
    161 
    162     def set_timeout_last(self):
    163         """The last highlight created will be removed after .5 sec"""
    164         # associate a counter with an event; only disable the "paren"
    165         # tag if the event is for the most recent timer.
    166         self.counter += 1
    167         self.editwin.text_frame.after(
    168             self.FLASH_DELAY,
    169             lambda self=self, c=self.counter: self.handle_restore_timer(c))
    170 
    171 
    172 if __name__ == '__main__':
    173     import unittest
    174     unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2)
    175