Commit | Line | Data |
---|---|---|
920dae64 AT |
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 | WARNING: This extension will fight with the CallTips extension, | |
8 | because they both are interested in the KeyRelease-parenright event. | |
9 | We'll have to fix IDLE to do something reasonable when two or more | |
10 | extensions what to capture the same event. | |
11 | """ | |
12 | ||
13 | import PyParse | |
14 | from EditorWindow import EditorWindow, index2line | |
15 | from configHandler import idleConf | |
16 | ||
17 | class ParenMatch: | |
18 | """Highlight matching parentheses | |
19 | ||
20 | There are three supported style of paren matching, based loosely | |
21 | on the Emacs options. The style is select based on the | |
22 | HILITE_STYLE attribute; it can be changed used the set_style | |
23 | method. | |
24 | ||
25 | The supported styles are: | |
26 | ||
27 | default -- When a right paren is typed, highlight the matching | |
28 | left paren for 1/2 sec. | |
29 | ||
30 | expression -- When a right paren is typed, highlight the entire | |
31 | expression from the left paren to the right paren. | |
32 | ||
33 | TODO: | |
34 | - fix interaction with CallTips | |
35 | - extend IDLE with configuration dialog to change options | |
36 | - implement rest of Emacs highlight styles (see below) | |
37 | - print mismatch warning in IDLE status window | |
38 | ||
39 | Note: In Emacs, there are several styles of highlight where the | |
40 | matching paren is highlighted whenever the cursor is immediately | |
41 | to the right of a right paren. I don't know how to do that in Tk, | |
42 | so I haven't bothered. | |
43 | """ | |
44 | menudefs = [] | |
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 | def __init__(self, editwin): | |
54 | self.editwin = editwin | |
55 | self.text = editwin.text | |
56 | self.finder = LastOpenBracketFinder(editwin) | |
57 | self.counter = 0 | |
58 | self._restore = None | |
59 | self.set_style(self.STYLE) | |
60 | ||
61 | def set_style(self, style): | |
62 | self.STYLE = style | |
63 | if style == "default": | |
64 | self.create_tag = self.create_tag_default | |
65 | self.set_timeout = self.set_timeout_last | |
66 | elif style == "expression": | |
67 | self.create_tag = self.create_tag_expression | |
68 | self.set_timeout = self.set_timeout_none | |
69 | ||
70 | def flash_open_paren_event(self, event): | |
71 | index = self.finder.find(keysym_type(event.keysym)) | |
72 | if index is None: | |
73 | self.warn_mismatched() | |
74 | return | |
75 | self._restore = 1 | |
76 | self.create_tag(index) | |
77 | self.set_timeout() | |
78 | ||
79 | def check_restore_event(self, event=None): | |
80 | if self._restore: | |
81 | self.text.tag_delete("paren") | |
82 | self._restore = None | |
83 | ||
84 | def handle_restore_timer(self, timer_count): | |
85 | if timer_count + 1 == self.counter: | |
86 | self.check_restore_event() | |
87 | ||
88 | def warn_mismatched(self): | |
89 | if self.BELL: | |
90 | self.text.bell() | |
91 | ||
92 | # any one of the create_tag_XXX methods can be used depending on | |
93 | # the style | |
94 | ||
95 | def create_tag_default(self, index): | |
96 | """Highlight the single paren that matches""" | |
97 | self.text.tag_add("paren", index) | |
98 | self.text.tag_config("paren", self.HILITE_CONFIG) | |
99 | ||
100 | def create_tag_expression(self, index): | |
101 | """Highlight the entire expression""" | |
102 | self.text.tag_add("paren", index, "insert") | |
103 | self.text.tag_config("paren", self.HILITE_CONFIG) | |
104 | ||
105 | # any one of the set_timeout_XXX methods can be used depending on | |
106 | # the style | |
107 | ||
108 | def set_timeout_none(self): | |
109 | """Highlight will remain until user input turns it off""" | |
110 | pass | |
111 | ||
112 | def set_timeout_last(self): | |
113 | """The last highlight created will be removed after .5 sec""" | |
114 | # associate a counter with an event; only disable the "paren" | |
115 | # tag if the event is for the most recent timer. | |
116 | self.editwin.text_frame.after(self.FLASH_DELAY, | |
117 | lambda self=self, c=self.counter: \ | |
118 | self.handle_restore_timer(c)) | |
119 | self.counter = self.counter + 1 | |
120 | ||
121 | def keysym_type(ks): | |
122 | # Not all possible chars or keysyms are checked because of the | |
123 | # limited context in which the function is used. | |
124 | if ks == "parenright" or ks == "(": | |
125 | return "paren" | |
126 | if ks == "bracketright" or ks == "[": | |
127 | return "bracket" | |
128 | if ks == "braceright" or ks == "{": | |
129 | return "brace" | |
130 | ||
131 | class LastOpenBracketFinder: | |
132 | num_context_lines = EditorWindow.num_context_lines | |
133 | indentwidth = EditorWindow.indentwidth | |
134 | tabwidth = EditorWindow.tabwidth | |
135 | context_use_ps1 = EditorWindow.context_use_ps1 | |
136 | ||
137 | def __init__(self, editwin): | |
138 | self.editwin = editwin | |
139 | self.text = editwin.text | |
140 | ||
141 | def _find_offset_in_buf(self, lno): | |
142 | y = PyParse.Parser(self.indentwidth, self.tabwidth) | |
143 | for context in self.num_context_lines: | |
144 | startat = max(lno - context, 1) | |
145 | startatindex = repr(startat) + ".0" | |
146 | # rawtext needs to contain everything up to the last | |
147 | # character, which was the close paren. the parser also | |
148 | # requires that the last line ends with "\n" | |
149 | rawtext = self.text.get(startatindex, "insert")[:-1] + "\n" | |
150 | y.set_str(rawtext) | |
151 | bod = y.find_good_parse_start( | |
152 | self.context_use_ps1, | |
153 | self._build_char_in_string_func(startatindex)) | |
154 | if bod is not None or startat == 1: | |
155 | break | |
156 | y.set_lo(bod or 0) | |
157 | i = y.get_last_open_bracket_pos() | |
158 | return i, y.str | |
159 | ||
160 | def find(self, right_keysym_type): | |
161 | """Return the location of the last open paren""" | |
162 | lno = index2line(self.text.index("insert")) | |
163 | i, buf = self._find_offset_in_buf(lno) | |
164 | if i is None \ | |
165 | or keysym_type(buf[i]) != right_keysym_type: | |
166 | return None | |
167 | lines_back = buf[i:].count("\n") - 1 | |
168 | # subtract one for the "\n" added to please the parser | |
169 | upto_open = buf[:i] | |
170 | j = upto_open.rfind("\n") + 1 # offset of column 0 of line | |
171 | offset = i - j | |
172 | return "%d.%d" % (lno - lines_back, offset) | |
173 | ||
174 | def _build_char_in_string_func(self, startindex): | |
175 | def inner(offset, startindex=startindex, | |
176 | icis=self.editwin.is_char_in_string): | |
177 | return icis(startindex + "%dc" % offset) | |
178 | return inner |