ParenMatch.py 6.47 KB
Newer Older
David Scherer's avatar
David Scherer committed
1 2 3 4 5 6 7
"""ParenMatch -- An IDLE extension for parenthesis matching.

When you hit a right paren, the cursor should move briefly to the left
paren.  Paren here is used generically; the matching applies to
parentheses, square brackets, and curly braces.
"""

8 9
from idlelib.HyperParser import HyperParser
from idlelib.configHandler import idleConf
David Scherer's avatar
David Scherer committed
10

11
_openers = {')':'(',']':'[','}':'{'}
12 13
CHECK_DELAY = 100 # miliseconds

David Scherer's avatar
David Scherer committed
14 15 16 17
class ParenMatch:
    """Highlight matching parentheses

    There are three supported style of paren matching, based loosely
18
    on the Emacs options.  The style is select based on the
David Scherer's avatar
David Scherer committed
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
    HILITE_STYLE attribute; it can be changed used the set_style
    method.

    The supported styles are:

    default -- When a right paren is typed, highlight the matching
        left paren for 1/2 sec.

    expression -- When a right paren is typed, highlight the entire
        expression from the left paren to the right paren.

    TODO:
        - extend IDLE with configuration dialog to change options
        - implement rest of Emacs highlight styles (see below)
        - print mismatch warning in IDLE status window

    Note: In Emacs, there are several styles of highlight where the
    matching paren is highlighted whenever the cursor is immediately
    to the right of a right paren.  I don't know how to do that in Tk,
    so I haven't bothered.
    """
40 41 42 43 44
    menudefs = [
        ('edit', [
            ("Show surrounding parens", "<<flash-paren>>"),
        ])
    ]
45 46 47 48 49 50 51
    STYLE = idleConf.GetOption('extensions','ParenMatch','style',
            default='expression')
    FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
            type='int',default=500)
    HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
    BELL = idleConf.GetOption('extensions','ParenMatch','bell',
            type='bool',default=1)
David Scherer's avatar
David Scherer committed
52

53 54 55 56 57 58
    RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>"
    # We want the restore event be called before the usual return and
    # backspace events.
    RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>",
                         "<Key-Return>", "<Key-BackSpace>")

David Scherer's avatar
David Scherer committed
59 60 61
    def __init__(self, editwin):
        self.editwin = editwin
        self.text = editwin.text
62 63 64 65 66
        # Bind the check-restore event to the function restore_event,
        # so that we can then use activate_restore (which calls event_add)
        # and deactivate_restore (which calls event_delete).
        editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME,
                          self.restore_event)
David Scherer's avatar
David Scherer committed
67
        self.counter = 0
68
        self.is_restore_active = 0
David Scherer's avatar
David Scherer committed
69 70
        self.set_style(self.STYLE)

71 72 73 74 75 76 77 78 79 80 81 82
    def activate_restore(self):
        if not self.is_restore_active:
            for seq in self.RESTORE_SEQUENCES:
                self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
            self.is_restore_active = True

    def deactivate_restore(self):
        if self.is_restore_active:
            for seq in self.RESTORE_SEQUENCES:
                self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq)
            self.is_restore_active = False

David Scherer's avatar
David Scherer committed
83 84 85 86 87 88 89 90 91
    def set_style(self, style):
        self.STYLE = style
        if style == "default":
            self.create_tag = self.create_tag_default
            self.set_timeout = self.set_timeout_last
        elif style == "expression":
            self.create_tag = self.create_tag_expression
            self.set_timeout = self.set_timeout_none

92 93 94
    def flash_paren_event(self, event):
        indices = HyperParser(self.editwin, "insert").get_surrounding_brackets()
        if indices is None:
David Scherer's avatar
David Scherer committed
95 96
            self.warn_mismatched()
            return
97 98 99 100 101 102
        self.activate_restore()
        self.create_tag(indices)
        self.set_timeout_last()

    def paren_closed_event(self, event):
        # If it was a shortcut and not really a closing paren, quit.
103 104
        closer = self.text.get("insert-1c")
        if closer not in _openers:
105 106 107 108
            return
        hp = HyperParser(self.editwin, "insert-1c")
        if not hp.is_in_code():
            return
109
        indices = hp.get_surrounding_brackets(_openers[closer], True)
110 111 112 113 114
        if indices is None:
            self.warn_mismatched()
            return
        self.activate_restore()
        self.create_tag(indices)
David Scherer's avatar
David Scherer committed
115 116
        self.set_timeout()

117 118 119 120
    def restore_event(self, event=None):
        self.text.tag_delete("paren")
        self.deactivate_restore()
        self.counter += 1   # disable the last timer, if there is one.
David Scherer's avatar
David Scherer committed
121 122

    def handle_restore_timer(self, timer_count):
123 124
        if timer_count == self.counter:
            self.restore_event()
David Scherer's avatar
David Scherer committed
125 126 127 128 129 130 131 132

    def warn_mismatched(self):
        if self.BELL:
            self.text.bell()

    # any one of the create_tag_XXX methods can be used depending on
    # the style

133
    def create_tag_default(self, indices):
David Scherer's avatar
David Scherer committed
134
        """Highlight the single paren that matches"""
135
        self.text.tag_add("paren", indices[0])
David Scherer's avatar
David Scherer committed
136 137
        self.text.tag_config("paren", self.HILITE_CONFIG)

138
    def create_tag_expression(self, indices):
David Scherer's avatar
David Scherer committed
139
        """Highlight the entire expression"""
140 141 142 143 144
        if self.text.get(indices[1]) in (')', ']', '}'):
            rightindex = indices[1]+"+1c"
        else:
            rightindex = indices[1]
        self.text.tag_add("paren", indices[0], rightindex)
David Scherer's avatar
David Scherer committed
145 146 147 148 149 150
        self.text.tag_config("paren", self.HILITE_CONFIG)

    # any one of the set_timeout_XXX methods can be used depending on
    # the style

    def set_timeout_none(self):
151 152 153 154 155 156 157 158 159 160 161 162 163
        """Highlight will remain until user input turns it off
        or the insert has moved"""
        # After CHECK_DELAY, call a function which disables the "paren" tag
        # if the event is for the most recent timer and the insert has changed,
        # or schedules another call for itself.
        self.counter += 1
        def callme(callme, self=self, c=self.counter,
                   index=self.text.index("insert")):
            if index != self.text.index("insert"):
                self.handle_restore_timer(c)
            else:
                self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
        self.editwin.text_frame.after(CHECK_DELAY, callme, callme)
David Scherer's avatar
David Scherer committed
164 165 166 167 168

    def set_timeout_last(self):
        """The last highlight created will be removed after .5 sec"""
        # associate a counter with an event; only disable the "paren"
        # tag if the event is for the most recent timer.
169
        self.counter += 1
David Scherer's avatar
David Scherer committed
170 171 172
        self.editwin.text_frame.after(self.FLASH_DELAY,
                                      lambda self=self, c=self.counter: \
                                      self.handle_restore_timer(c))