blob: 1be60c0fdab8c48a12e5fb19008014fef427f8ce [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
Jeremy Hylton63c2b252000-03-02 19:06:57 +000013import PyParse
14from AutoIndent import AutoIndent, index2line
Jeremy Hylton6b3edf02000-03-07 17:55:32 +000015from IdleConf import idleconf
Jeremy Hylton63c2b252000-03-02 19:06:57 +000016
17class ParenMatch:
18 """Highlight matching parentheses
19
20 There are three supported style of paren matching, based loosely
Tim Peters70c43782001-01-17 08:48:39 +000021 on the Emacs options. The style is select based on the
Jeremy Hylton63c2b252000-03-02 19:06:57 +000022 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 """
Tim Peters70c43782001-01-17 08:48:39 +000044
Jeremy Hylton63c2b252000-03-02 19:06:57 +000045 menudefs = []
Tim Peters70c43782001-01-17 08:48:39 +000046
Jeremy Hylton63c2b252000-03-02 19:06:57 +000047 keydefs = {
48 '<<flash-open-paren>>' : ('<KeyRelease-parenright>',
49 '<KeyRelease-bracketright>',
50 '<KeyRelease-braceright>'),
51 '<<check-restore>>' : ('<KeyPress>',),
52 }
53
54 windows_keydefs = {}
55 unix_keydefs = {}
56
Jeremy Hylton6b3edf02000-03-07 17:55:32 +000057 iconf = idleconf.getsection('ParenMatch')
58 STYLE = iconf.getdef('style', 'default')
Jeremy Hyltone81f28b2000-03-03 23:06:45 +000059 FLASH_DELAY = iconf.getint('flash-delay')
60 HILITE_CONFIG = iconf.getcolor('hilite')
61 BELL = iconf.getboolean('bell')
62 del iconf
Jeremy Hylton63c2b252000-03-02 19:06:57 +000063
64 def __init__(self, editwin):
65 self.editwin = editwin
66 self.text = editwin.text
67 self.finder = LastOpenBracketFinder(editwin)
68 self.counter = 0
69 self._restore = None
70 self.set_style(self.STYLE)
71
72 def set_style(self, style):
73 self.STYLE = style
74 if style == "default":
75 self.create_tag = self.create_tag_default
76 self.set_timeout = self.set_timeout_last
77 elif style == "expression":
78 self.create_tag = self.create_tag_expression
79 self.set_timeout = self.set_timeout_none
80
81 def flash_open_paren_event(self, event):
82 index = self.finder.find(keysym_type(event.keysym))
83 if index is None:
84 self.warn_mismatched()
85 return
86 self._restore = 1
87 self.create_tag(index)
88 self.set_timeout()
89
90 def check_restore_event(self, event=None):
91 if self._restore:
92 self.text.tag_delete("paren")
93 self._restore = None
94
95 def handle_restore_timer(self, timer_count):
96 if timer_count + 1 == self.counter:
97 self.check_restore_event()
98
99 def warn_mismatched(self):
100 if self.BELL:
101 self.text.bell()
102
103 # any one of the create_tag_XXX methods can be used depending on
104 # the style
105
106 def create_tag_default(self, index):
107 """Highlight the single paren that matches"""
108 self.text.tag_add("paren", index)
109 self.text.tag_config("paren", self.HILITE_CONFIG)
110
111 def create_tag_expression(self, index):
112 """Highlight the entire expression"""
113 self.text.tag_add("paren", index, "insert")
114 self.text.tag_config("paren", self.HILITE_CONFIG)
115
116 # any one of the set_timeout_XXX methods can be used depending on
117 # the style
118
119 def set_timeout_none(self):
120 """Highlight will remain until user input turns it off"""
121 pass
122
123 def set_timeout_last(self):
124 """The last highlight created will be removed after .5 sec"""
125 # associate a counter with an event; only disable the "paren"
126 # tag if the event is for the most recent timer.
127 self.editwin.text_frame.after(self.FLASH_DELAY,
128 lambda self=self, c=self.counter: \
129 self.handle_restore_timer(c))
130 self.counter = self.counter + 1
131
132def keysym_type(ks):
133 # Not all possible chars or keysyms are checked because of the
134 # limited context in which the function is used.
135 if ks == "parenright" or ks == "(":
136 return "paren"
137 if ks == "bracketright" or ks == "[":
138 return "bracket"
139 if ks == "braceright" or ks == "{":
140 return "brace"
141
142class LastOpenBracketFinder:
143 num_context_lines = AutoIndent.num_context_lines
144 indentwidth = AutoIndent.indentwidth
145 tabwidth = AutoIndent.tabwidth
146 context_use_ps1 = AutoIndent.context_use_ps1
Tim Peters70c43782001-01-17 08:48:39 +0000147
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000148 def __init__(self, editwin):
149 self.editwin = editwin
150 self.text = editwin.text
151
152 def _find_offset_in_buf(self, lno):
153 y = PyParse.Parser(self.indentwidth, self.tabwidth)
154 for context in self.num_context_lines:
155 startat = max(lno - context, 1)
156 startatindex = `startat` + ".0"
157 # rawtext needs to contain everything up to the last
Jeremy Hyltone81f28b2000-03-03 23:06:45 +0000158 # character, which was the close paren. the parser also
Tim Peters70c43782001-01-17 08:48:39 +0000159 # requires that the last line ends with "\n"
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000160 rawtext = self.text.get(startatindex, "insert")[:-1] + "\n"
161 y.set_str(rawtext)
162 bod = y.find_good_parse_start(
163 self.context_use_ps1,
164 self._build_char_in_string_func(startatindex))
165 if bod is not None or startat == 1:
166 break
167 y.set_lo(bod or 0)
168 i = y.get_last_open_bracket_pos()
169 return i, y.str
170
171 def find(self, right_keysym_type):
172 """Return the location of the last open paren"""
173 lno = index2line(self.text.index("insert"))
174 i, buf = self._find_offset_in_buf(lno)
Jeremy Hyltone81f28b2000-03-03 23:06:45 +0000175 if i is None \
Tim Peters70c43782001-01-17 08:48:39 +0000176 or keysym_type(buf[i]) != right_keysym_type:
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000177 return None
Walter Dörwaldaaab30e2002-09-11 20:36:02 +0000178 lines_back = buf[i:].count("\n") - 1
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000179 # subtract one for the "\n" added to please the parser
180 upto_open = buf[:i]
Walter Dörwaldaaab30e2002-09-11 20:36:02 +0000181 j = upto_open.rfind("\n") + 1 # offset of column 0 of line
Jeremy Hylton63c2b252000-03-02 19:06:57 +0000182 offset = i - j
183 return "%d.%d" % (lno - lines_back, offset)
184
185 def _build_char_in_string_func(self, startindex):
186 def inner(offset, startindex=startindex,
187 icis=self.editwin.is_char_in_string):
188 return icis(startindex + "%dc" % offset)
189 return inner