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