1"""ParenMatch -- 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 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 178ParenMatch.reload() 179 180 181if __name__ == '__main__': 182 from unittest import main 183 main('idlelib.idle_test.test_parenmatch', verbosity=2) 184