blob: 17d76c2659c53cd91a00061f031e8e3b96a6850d [file] [log] [blame]
Jeremy Hylton63c2b252000-03-02 19:06:57 +00001"""ParenMatch -- An IDLE extension for parenthesis matching.
2
3When you hit a right paren, the cursor should move briefly to the left
4paren. Paren here is used generically; the matching applies to
5parentheses, square brackets, and curly braces.
6
7WARNING: This extension will fight with the CallTips extension,
8because they both are interested in the KeyRelease-parenright event.
9We'll have to fix IDLE to do something reasonable when two or more
10extensions what to capture the same event.
11"""
12
13import string
14
15import PyParse
16from AutoIndent import AutoIndent, index2line
Jeremy Hylton6b3edf02000-03-07 17:55:32 +000017from IdleConf import idleconf
Jeremy Hylton63c2b252000-03-02 19:06:57 +000018
19class ParenMatch:
20 """Highlight matching parentheses
21
22 There are three supported style of paren matching, based loosely
Tim Peters70c43782001-01-17 08:48:39 +000023 on the Emacs options. The style is select based on the
Jeremy Hylton63c2b252000-03-02 19:06:57 +000024 HILITE_STYLE attribute; it can be changed used the set_style
25 method.
26
27 The supported styles are:
28
29 default -- When a right paren is typed, highlight the matching
30 left paren for 1/2 sec.
31
32 expression -- When a right paren is typed, highlight the entire
33 expression from the left paren to the right paren.
34
35 TODO:
36 - fix interaction with CallTips
37 - extend IDLE with configuration dialog to change options
38 - implement rest of Emacs highlight styles (see below)
39 - print mismatch warning in IDLE status window
40
41 Note: In Emacs, there are several styles of highlight where the
42 matching paren is highlighted whenever the cursor is immediately
43 to the right of a right paren. I don't know how to do that in Tk,
44 so I haven't bothered.
45 """
Tim Peters70c43782001-01-17 08:48:39 +000046
Jeremy Hylton63c2b252000-03-02 19:06:57 +000047 menudefs = []
Tim Peters70c43782001-01-17 08:48:39 +000048
Jeremy Hylton63c2b252000-03-02 19:06:57 +000049 keydefs = {
50 '<<flash-open-paren>>' : ('<KeyRelease-parenright>',
51 '<KeyRelease-bracketright>',
52 '<KeyRelease-braceright>'),
53 '<<check-restore>>' : ('<KeyPress>',),
54 }
55
56 windows_keydefs = {}
57 unix_keydefs = {}
58
Jeremy Hylton6b3edf02000-03-07 17:55:32 +000059 iconf = idleconf.getsection('ParenMatch')
60 STYLE = iconf.getdef('style', 'default')
Jeremy Hyltone81f28b2000-03-03 23:06:45 +000061 FLASH_DELAY = iconf.getint('flash-delay')
62 HILITE_CONFIG = iconf.getcolor('hilite')
63 BELL = iconf.getboolean('bell')
64 del iconf
Jeremy Hylton63c2b252000-03-02 19:06:57 +000065
66 def __init__(self, editwin):
67 self.editwin = editwin
68 self.text = editwin.text
69 self.finder = LastOpenBracketFinder(editwin)
70 self.counter = 0
71 self._restore = None
72 self.set_style(self.STYLE)
73
74 def set_style(self, style):
75 self.STYLE = style
76 if style == "default":
77 self.create_tag = self.create_tag_default
78 self.set_timeout = self.set_timeout_last
79 elif style == "expression":
80 self.create_tag = self.create_tag_expression
81 self.set_timeout = self.set_timeout_none
82
83 def flash_open_paren_event(self, event):
84 index = self.finder.find(keysym_type(event.keysym))
85 if index is None:
86 self.warn_mismatched()
87 return
88 self._restore = 1
89 self.create_tag(index)
90 self.set_timeout()
91
92 def check_restore_event(self, event=None):
93 if self._restore:
94 self.text.tag_delete("paren")
95 self._restore = None
96
97 def handle_restore_timer(self, timer_count):
98 if timer_count + 1 == self.counter:
99 self.check_restore_event()
100
101 def warn_mismatched(self):
102 if self.BELL:
103 self.text.bell()
104
105 # any one of the create_tag_XXX methods can be used depending on
106 # the style
107
108 def create_tag_default(self, index):
109 """Highlight the single paren that matches"""
110 self.text.tag_add("paren", index)
111 self.text.tag_config("paren", self.HILITE_CONFIG)
112
113 def create_tag_expression(self, index):
114 """Highlight the entire expression"""
115 self.text.tag_add("paren", index, "insert")
116 self.text.tag_config("paren", self.HILITE_CONFIG)
117
118 # any one of the set_timeout_XXX methods can be used depending on
119 # the style
120
121 def set_timeout_none(self):
122 """Highlight will remain until user input turns it off"""
123 pass
124
125 def set_timeout_last(self):
126 """The last highlight created will be removed after .5 sec"""
127 # associate a counter with an event; only disable the "paren"
128 # tag if the event is for the most recent timer.
129 self.editwin.text_frame.after(self.FLASH_DELAY,
130 lambda self=self, c=self.counter: \
131 self.handle_restore_timer(c))
132 self.counter = self.counter + 1
133
134def keysym_type(ks):
135 # Not all possible chars or keysyms are checked because of the
136 # limited context in which the function is used.
137 if ks == "parenright" or ks == "(":
138 return "paren"
139 if ks == "bracketright" or ks == "[":
140 return "bracket"
141 if ks == "braceright" or ks == "{":
142 return "brace"
143
144class LastOpenBracketFinder:
145 num_context_lines = AutoIndent.num_context_lines
146 indentwidth = AutoIndent.indentwidth
147 tabwidth = AutoIndent.tabwidth
148 context_use_ps1 = AutoIndent.context_use_ps1
Tim Peters70c43782001-01-17 08:48:39 +0000149
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000150 def __init__(self, editwin):
151 self.editwin = editwin
152 self.text = editwin.text
153
154 def _find_offset_in_buf(self, lno):
155 y = PyParse.Parser(self.indentwidth, self.tabwidth)
156 for context in self.num_context_lines:
157 startat = max(lno - context, 1)
158 startatindex = `startat` + ".0"
159 # rawtext needs to contain everything up to the last
Jeremy Hyltone81f28b2000-03-03 23:06:45 +0000160 # character, which was the close paren. the parser also
Tim Peters70c43782001-01-17 08:48:39 +0000161 # requires that the last line ends with "\n"
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000162 rawtext = self.text.get(startatindex, "insert")[:-1] + "\n"
163 y.set_str(rawtext)
164 bod = y.find_good_parse_start(
165 self.context_use_ps1,
166 self._build_char_in_string_func(startatindex))
167 if bod is not None or startat == 1:
168 break
169 y.set_lo(bod or 0)
170 i = y.get_last_open_bracket_pos()
171 return i, y.str
172
173 def find(self, right_keysym_type):
174 """Return the location of the last open paren"""
175 lno = index2line(self.text.index("insert"))
176 i, buf = self._find_offset_in_buf(lno)
Jeremy Hyltone81f28b2000-03-03 23:06:45 +0000177 if i is None \
Tim Peters70c43782001-01-17 08:48:39 +0000178 or keysym_type(buf[i]) != right_keysym_type:
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000179 return None
Guido van Rossum074d6e12000-03-06 14:16:41 +0000180 lines_back = string.count(buf[i:], "\n") - 1
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000181 # subtract one for the "\n" added to please the parser
182 upto_open = buf[:i]
Guido van Rossum074d6e12000-03-06 14:16:41 +0000183 j = string.rfind(upto_open, "\n") + 1 # offset of column 0 of line
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000184 offset = i - j
185 return "%d.%d" % (lno - lines_back, offset)
186
187 def _build_char_in_string_func(self, startindex):
188 def inner(offset, startindex=startindex,
189 icis=self.editwin.is_char_in_string):
190 return icis(startindex + "%dc" % offset)
191 return inner