jpayne@69: """ParenMatch -- for parenthesis matching. jpayne@69: jpayne@69: When you hit a right paren, the cursor should move briefly to the left jpayne@69: paren. Paren here is used generically; the matching applies to jpayne@69: parentheses, square brackets, and curly braces. jpayne@69: """ jpayne@69: from idlelib.hyperparser import HyperParser jpayne@69: from idlelib.config import idleConf jpayne@69: jpayne@69: _openers = {')':'(',']':'[','}':'{'} jpayne@69: CHECK_DELAY = 100 # milliseconds jpayne@69: jpayne@69: class ParenMatch: jpayne@69: """Highlight matching openers and closers, (), [], and {}. jpayne@69: jpayne@69: There are three supported styles of paren matching. When a right jpayne@69: paren (opener) is typed: jpayne@69: jpayne@69: opener -- highlight the matching left paren (closer); jpayne@69: parens -- highlight the left and right parens (opener and closer); jpayne@69: expression -- highlight the entire expression from opener to closer. jpayne@69: (For back compatibility, 'default' is a synonym for 'opener'). jpayne@69: jpayne@69: Flash-delay is the maximum milliseconds the highlighting remains. jpayne@69: Any cursor movement (key press or click) before that removes the jpayne@69: highlight. If flash-delay is 0, there is no maximum. jpayne@69: jpayne@69: TODO: jpayne@69: - Augment bell() with mismatch warning in status window. jpayne@69: - Highlight when cursor is moved to the right of a closer. jpayne@69: This might be too expensive to check. jpayne@69: """ jpayne@69: jpayne@69: RESTORE_VIRTUAL_EVENT_NAME = "<>" jpayne@69: # We want the restore event be called before the usual return and jpayne@69: # backspace events. jpayne@69: RESTORE_SEQUENCES = ("", "", jpayne@69: "", "") jpayne@69: jpayne@69: def __init__(self, editwin): jpayne@69: self.editwin = editwin jpayne@69: self.text = editwin.text jpayne@69: # Bind the check-restore event to the function restore_event, jpayne@69: # so that we can then use activate_restore (which calls event_add) jpayne@69: # and deactivate_restore (which calls event_delete). jpayne@69: editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, jpayne@69: self.restore_event) jpayne@69: self.counter = 0 jpayne@69: self.is_restore_active = 0 jpayne@69: jpayne@69: @classmethod jpayne@69: def reload(cls): jpayne@69: cls.STYLE = idleConf.GetOption( jpayne@69: 'extensions','ParenMatch','style', default='opener') jpayne@69: cls.FLASH_DELAY = idleConf.GetOption( jpayne@69: 'extensions','ParenMatch','flash-delay', type='int',default=500) jpayne@69: cls.BELL = idleConf.GetOption( jpayne@69: 'extensions','ParenMatch','bell', type='bool', default=1) jpayne@69: cls.HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(), jpayne@69: 'hilite') jpayne@69: jpayne@69: def activate_restore(self): jpayne@69: "Activate mechanism to restore text from highlighting." jpayne@69: if not self.is_restore_active: jpayne@69: for seq in self.RESTORE_SEQUENCES: jpayne@69: self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq) jpayne@69: self.is_restore_active = True jpayne@69: jpayne@69: def deactivate_restore(self): jpayne@69: "Remove restore event bindings." jpayne@69: if self.is_restore_active: jpayne@69: for seq in self.RESTORE_SEQUENCES: jpayne@69: self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq) jpayne@69: self.is_restore_active = False jpayne@69: jpayne@69: def flash_paren_event(self, event): jpayne@69: "Handle editor 'show surrounding parens' event (menu or shortcut)." jpayne@69: indices = (HyperParser(self.editwin, "insert") jpayne@69: .get_surrounding_brackets()) jpayne@69: self.finish_paren_event(indices) jpayne@69: return "break" jpayne@69: jpayne@69: def paren_closed_event(self, event): jpayne@69: "Handle user input of closer." jpayne@69: # If user bound non-closer to <>, quit. jpayne@69: closer = self.text.get("insert-1c") jpayne@69: if closer not in _openers: jpayne@69: return jpayne@69: hp = HyperParser(self.editwin, "insert-1c") jpayne@69: if not hp.is_in_code(): jpayne@69: return jpayne@69: indices = hp.get_surrounding_brackets(_openers[closer], True) jpayne@69: self.finish_paren_event(indices) jpayne@69: return # Allow calltips to see ')' jpayne@69: jpayne@69: def finish_paren_event(self, indices): jpayne@69: if indices is None and self.BELL: jpayne@69: self.text.bell() jpayne@69: return jpayne@69: self.activate_restore() jpayne@69: # self.create_tag(indices) jpayne@69: self.tagfuncs.get(self.STYLE, self.create_tag_expression)(self, indices) jpayne@69: # self.set_timeout() jpayne@69: (self.set_timeout_last if self.FLASH_DELAY else jpayne@69: self.set_timeout_none)() jpayne@69: jpayne@69: def restore_event(self, event=None): jpayne@69: "Remove effect of doing match." jpayne@69: self.text.tag_delete("paren") jpayne@69: self.deactivate_restore() jpayne@69: self.counter += 1 # disable the last timer, if there is one. jpayne@69: jpayne@69: def handle_restore_timer(self, timer_count): jpayne@69: if timer_count == self.counter: jpayne@69: self.restore_event() jpayne@69: jpayne@69: # any one of the create_tag_XXX methods can be used depending on jpayne@69: # the style jpayne@69: jpayne@69: def create_tag_opener(self, indices): jpayne@69: """Highlight the single paren that matches""" jpayne@69: self.text.tag_add("paren", indices[0]) jpayne@69: self.text.tag_config("paren", self.HILITE_CONFIG) jpayne@69: jpayne@69: def create_tag_parens(self, indices): jpayne@69: """Highlight the left and right parens""" jpayne@69: if self.text.get(indices[1]) in (')', ']', '}'): jpayne@69: rightindex = indices[1]+"+1c" jpayne@69: else: jpayne@69: rightindex = indices[1] jpayne@69: self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex) jpayne@69: self.text.tag_config("paren", self.HILITE_CONFIG) jpayne@69: jpayne@69: def create_tag_expression(self, indices): jpayne@69: """Highlight the entire expression""" jpayne@69: if self.text.get(indices[1]) in (')', ']', '}'): jpayne@69: rightindex = indices[1]+"+1c" jpayne@69: else: jpayne@69: rightindex = indices[1] jpayne@69: self.text.tag_add("paren", indices[0], rightindex) jpayne@69: self.text.tag_config("paren", self.HILITE_CONFIG) jpayne@69: jpayne@69: tagfuncs = { jpayne@69: 'opener': create_tag_opener, jpayne@69: 'default': create_tag_opener, jpayne@69: 'parens': create_tag_parens, jpayne@69: 'expression': create_tag_expression, jpayne@69: } jpayne@69: jpayne@69: # any one of the set_timeout_XXX methods can be used depending on jpayne@69: # the style jpayne@69: jpayne@69: def set_timeout_none(self): jpayne@69: """Highlight will remain until user input turns it off jpayne@69: or the insert has moved""" jpayne@69: # After CHECK_DELAY, call a function which disables the "paren" tag jpayne@69: # if the event is for the most recent timer and the insert has changed, jpayne@69: # or schedules another call for itself. jpayne@69: self.counter += 1 jpayne@69: def callme(callme, self=self, c=self.counter, jpayne@69: index=self.text.index("insert")): jpayne@69: if index != self.text.index("insert"): jpayne@69: self.handle_restore_timer(c) jpayne@69: else: jpayne@69: self.editwin.text_frame.after(CHECK_DELAY, callme, callme) jpayne@69: self.editwin.text_frame.after(CHECK_DELAY, callme, callme) jpayne@69: jpayne@69: def set_timeout_last(self): jpayne@69: """The last highlight created will be removed after FLASH_DELAY millisecs""" jpayne@69: # associate a counter with an event; only disable the "paren" jpayne@69: # tag if the event is for the most recent timer. jpayne@69: self.counter += 1 jpayne@69: self.editwin.text_frame.after( jpayne@69: self.FLASH_DELAY, jpayne@69: lambda self=self, c=self.counter: self.handle_restore_timer(c)) jpayne@69: jpayne@69: jpayne@69: ParenMatch.reload() jpayne@69: jpayne@69: jpayne@69: if __name__ == '__main__': jpayne@69: from unittest import main jpayne@69: main('idlelib.idle_test.test_parenmatch', verbosity=2)