14adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao"""ParenMatch -- An IDLE extension for parenthesis matching. 24adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 34adfde8bc82dd39f59e0445588c3e599ada477dJosh GaoWhen you hit a right paren, the cursor should move briefly to the left 44adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoparen. Paren here is used generically; the matching applies to 54adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoparentheses, square brackets, and curly braces. 64adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao""" 74adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 84adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaofrom idlelib.HyperParser import HyperParser 94adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaofrom idlelib.configHandler import idleConf 104adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 114adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao_openers = {')':'(',']':'[','}':'{'} 124adfde8bc82dd39f59e0445588c3e599ada477dJosh GaoCHECK_DELAY = 100 # miliseconds 134adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 144adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoclass ParenMatch: 154adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao """Highlight matching parentheses 164adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 174adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao There are three supported style of paren matching, based loosely 184adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao on the Emacs options. The style is select based on the 194adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao HILITE_STYLE attribute; it can be changed used the set_style 204adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao method. 214adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 224adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao The supported styles are: 234adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 244adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao default -- When a right paren is typed, highlight the matching 254adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao left paren for 1/2 sec. 264adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 274adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao expression -- When a right paren is typed, highlight the entire 284adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao expression from the left paren to the right paren. 294adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 304adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao TODO: 314adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao - extend IDLE with configuration dialog to change options 324adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao - implement rest of Emacs highlight styles (see below) 334adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao - print mismatch warning in IDLE status window 344adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 354adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao Note: In Emacs, there are several styles of highlight where the 364adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao matching paren is highlighted whenever the cursor is immediately 374adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao to the right of a right paren. I don't know how to do that in Tk, 384adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao so I haven't bothered. 394adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao """ 404adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao menudefs = [ 414adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao ('edit', [ 424adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao ("Show surrounding parens", "<<flash-paren>>"), 434adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao ]) 444adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao ] 454adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao STYLE = idleConf.GetOption('extensions','ParenMatch','style', 464adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao default='expression') 474adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay', 484adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao type='int',default=500) 494adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite') 504adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao BELL = idleConf.GetOption('extensions','ParenMatch','bell', 514adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao type='bool',default=1) 524adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 534adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>" 544adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # We want the restore event be called before the usual return and 554adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # backspace events. 564adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>", 574adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao "<Key-Return>", "<Key-BackSpace>") 584adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 594adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def __init__(self, editwin): 604adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.editwin = editwin 614adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text = editwin.text 624adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # Bind the check-restore event to the function restore_event, 634adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # so that we can then use activate_restore (which calls event_add) 644adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # and deactivate_restore (which calls event_delete). 654adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, 664adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.restore_event) 674adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.counter = 0 684adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.is_restore_active = 0 694adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.set_style(self.STYLE) 704adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 714adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def activate_restore(self): 724adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if not self.is_restore_active: 734adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao for seq in self.RESTORE_SEQUENCES: 744adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq) 754adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.is_restore_active = True 764adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 774adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def deactivate_restore(self): 784adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if self.is_restore_active: 794adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao for seq in self.RESTORE_SEQUENCES: 804adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq) 814adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.is_restore_active = False 824adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 834adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def set_style(self, style): 844adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.STYLE = style 854adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if style == "default": 864adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.create_tag = self.create_tag_default 874adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.set_timeout = self.set_timeout_last 884adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao elif style == "expression": 894adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.create_tag = self.create_tag_expression 904adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.set_timeout = self.set_timeout_none 914adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 924adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def flash_paren_event(self, event): 934adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao indices = HyperParser(self.editwin, "insert").get_surrounding_brackets() 944adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if indices is None: 954adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.warn_mismatched() 964adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao return 974adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.activate_restore() 984adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.create_tag(indices) 994adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.set_timeout_last() 1004adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1014adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def paren_closed_event(self, event): 1024adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # If it was a shortcut and not really a closing paren, quit. 1034adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao closer = self.text.get("insert-1c") 1044adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if closer not in _openers: 1054adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao return 1064adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao hp = HyperParser(self.editwin, "insert-1c") 1074adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if not hp.is_in_code(): 1084adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao return 1094adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao indices = hp.get_surrounding_brackets(_openers[closer], True) 1104adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if indices is None: 1114adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.warn_mismatched() 1124adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao return 1134adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.activate_restore() 1144adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.create_tag(indices) 1154adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.set_timeout() 1164adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1174adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def restore_event(self, event=None): 1184adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.tag_delete("paren") 1194adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.deactivate_restore() 1204adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.counter += 1 # disable the last timer, if there is one. 1214adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1224adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def handle_restore_timer(self, timer_count): 1234adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if timer_count == self.counter: 1244adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.restore_event() 1254adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1264adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def warn_mismatched(self): 1274adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if self.BELL: 1284adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.bell() 1294adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1304adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # any one of the create_tag_XXX methods can be used depending on 1314adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # the style 1324adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1334adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def create_tag_default(self, indices): 1344adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao """Highlight the single paren that matches""" 1354adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.tag_add("paren", indices[0]) 1364adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.tag_config("paren", self.HILITE_CONFIG) 1374adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1384adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def create_tag_expression(self, indices): 1394adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao """Highlight the entire expression""" 1404adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if self.text.get(indices[1]) in (')', ']', '}'): 1414adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao rightindex = indices[1]+"+1c" 1424adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao else: 1434adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao rightindex = indices[1] 1444adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.tag_add("paren", indices[0], rightindex) 1454adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.text.tag_config("paren", self.HILITE_CONFIG) 1464adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1474adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # any one of the set_timeout_XXX methods can be used depending on 1484adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # the style 1494adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1504adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def set_timeout_none(self): 1514adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao """Highlight will remain until user input turns it off 1524adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao or the insert has moved""" 1534adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # After CHECK_DELAY, call a function which disables the "paren" tag 1544adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # if the event is for the most recent timer and the insert has changed, 1554adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # or schedules another call for itself. 1564adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.counter += 1 1574adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def callme(callme, self=self, c=self.counter, 1584adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao index=self.text.index("insert")): 1594adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao if index != self.text.index("insert"): 1604adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.handle_restore_timer(c) 1614adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao else: 1624adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.editwin.text_frame.after(CHECK_DELAY, callme, callme) 1634adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.editwin.text_frame.after(CHECK_DELAY, callme, callme) 1644adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao 1654adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao def set_timeout_last(self): 1664adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao """The last highlight created will be removed after .5 sec""" 1674adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # associate a counter with an event; only disable the "paren" 1684adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao # tag if the event is for the most recent timer. 1694adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.counter += 1 1704adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.editwin.text_frame.after(self.FLASH_DELAY, 1714adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao lambda self=self, c=self.counter: \ 1724adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao self.handle_restore_timer(c)) 173