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