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