blob: db6fb6c8007149744b4935ac013993eb2277c661 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +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
Kurt B. Kaiser8c11f7e2002-09-14 02:46:19 +000017from configHandler import idleConf
David Scherer7aced172000-08-15 01:13:23 +000018
19class ParenMatch:
20 """Highlight matching parentheses
21
22 There are three supported style of paren matching, based loosely
Kurt B. Kaiser4d4d2122001-07-13 19:49:27 +000023 on the Emacs options. The style is select based on the
David Scherer7aced172000-08-15 01:13:23 +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 """
David Scherer7aced172000-08-15 01:13:23 +000046 menudefs = []
Kurt B. Kaiser8c11f7e2002-09-14 02:46:19 +000047 STYLE = idleConf.GetOption('extensions','ParenMatch','style',
48 default='expression')
49 FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
50 type='int',default=500)
51 HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
52 BELL = idleConf.GetOption('extensions','ParenMatch','bell',
53 type='bool',default=1)
David Scherer7aced172000-08-15 01:13:23 +000054
55 def __init__(self, editwin):
56 self.editwin = editwin
57 self.text = editwin.text
58 self.finder = LastOpenBracketFinder(editwin)
59 self.counter = 0
60 self._restore = None
61 self.set_style(self.STYLE)
62
63 def set_style(self, style):
64 self.STYLE = style
65 if style == "default":
66 self.create_tag = self.create_tag_default
67 self.set_timeout = self.set_timeout_last
68 elif style == "expression":
69 self.create_tag = self.create_tag_expression
70 self.set_timeout = self.set_timeout_none
71
72 def flash_open_paren_event(self, event):
73 index = self.finder.find(keysym_type(event.keysym))
74 if index is None:
75 self.warn_mismatched()
76 return
77 self._restore = 1
78 self.create_tag(index)
79 self.set_timeout()
80
81 def check_restore_event(self, event=None):
82 if self._restore:
83 self.text.tag_delete("paren")
84 self._restore = None
85
86 def handle_restore_timer(self, timer_count):
87 if timer_count + 1 == self.counter:
88 self.check_restore_event()
89
90 def warn_mismatched(self):
91 if self.BELL:
92 self.text.bell()
93
94 # any one of the create_tag_XXX methods can be used depending on
95 # the style
96
97 def create_tag_default(self, index):
98 """Highlight the single paren that matches"""
99 self.text.tag_add("paren", index)
100 self.text.tag_config("paren", self.HILITE_CONFIG)
101
102 def create_tag_expression(self, index):
103 """Highlight the entire expression"""
104 self.text.tag_add("paren", index, "insert")
105 self.text.tag_config("paren", self.HILITE_CONFIG)
106
107 # any one of the set_timeout_XXX methods can be used depending on
108 # the style
109
110 def set_timeout_none(self):
111 """Highlight will remain until user input turns it off"""
112 pass
113
114 def set_timeout_last(self):
115 """The last highlight created will be removed after .5 sec"""
116 # associate a counter with an event; only disable the "paren"
117 # tag if the event is for the most recent timer.
118 self.editwin.text_frame.after(self.FLASH_DELAY,
119 lambda self=self, c=self.counter: \
120 self.handle_restore_timer(c))
121 self.counter = self.counter + 1
122
123def keysym_type(ks):
124 # Not all possible chars or keysyms are checked because of the
125 # limited context in which the function is used.
126 if ks == "parenright" or ks == "(":
127 return "paren"
128 if ks == "bracketright" or ks == "[":
129 return "bracket"
130 if ks == "braceright" or ks == "{":
131 return "brace"
132
133class LastOpenBracketFinder:
134 num_context_lines = AutoIndent.num_context_lines
135 indentwidth = AutoIndent.indentwidth
136 tabwidth = AutoIndent.tabwidth
137 context_use_ps1 = AutoIndent.context_use_ps1
Kurt B. Kaiser4d4d2122001-07-13 19:49:27 +0000138
David Scherer7aced172000-08-15 01:13:23 +0000139 def __init__(self, editwin):
140 self.editwin = editwin
141 self.text = editwin.text
142
143 def _find_offset_in_buf(self, lno):
144 y = PyParse.Parser(self.indentwidth, self.tabwidth)
145 for context in self.num_context_lines:
146 startat = max(lno - context, 1)
147 startatindex = `startat` + ".0"
148 # rawtext needs to contain everything up to the last
149 # character, which was the close paren. the parser also
Kurt B. Kaiser4d4d2122001-07-13 19:49:27 +0000150 # requires that the last line ends with "\n"
David Scherer7aced172000-08-15 01:13:23 +0000151 rawtext = self.text.get(startatindex, "insert")[:-1] + "\n"
152 y.set_str(rawtext)
153 bod = y.find_good_parse_start(
154 self.context_use_ps1,
155 self._build_char_in_string_func(startatindex))
156 if bod is not None or startat == 1:
157 break
158 y.set_lo(bod or 0)
159 i = y.get_last_open_bracket_pos()
160 return i, y.str
161
162 def find(self, right_keysym_type):
163 """Return the location of the last open paren"""
164 lno = index2line(self.text.index("insert"))
165 i, buf = self._find_offset_in_buf(lno)
166 if i is None \
Kurt B. Kaiser4d4d2122001-07-13 19:49:27 +0000167 or keysym_type(buf[i]) != right_keysym_type:
David Scherer7aced172000-08-15 01:13:23 +0000168 return None
169 lines_back = string.count(buf[i:], "\n") - 1
170 # subtract one for the "\n" added to please the parser
171 upto_open = buf[:i]
172 j = string.rfind(upto_open, "\n") + 1 # offset of column 0 of line
173 offset = i - j
174 return "%d.%d" % (lno - lines_back, offset)
175
176 def _build_char_in_string_func(self, startindex):
177 def inner(offset, startindex=startindex,
178 icis=self.editwin.is_char_in_string):
179 return icis(startindex + "%dc" % offset)
180 return inner