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