1"""ParenMatch -- An IDLE extension for parenthesis matching. 2 3When you hit a right paren, the cursor should move briefly to the left 4paren. Paren here is used generically; the matching applies to 5parentheses, square brackets, and curly braces. 6""" 7from idlelib.hyperparser import HyperParser 8from idlelib.config import idleConf 9 10_openers = {')':'(',']':'[','}':'{'} 11CHECK_DELAY = 100 # milliseconds 12 13class 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 172if __name__ == '__main__': 173 import unittest 174 unittest.main('idlelib.idle_test.test_parenmatch', verbosity=2) 175