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))
|
---|