blob: 75006037b78214f7cf195ceb549ea3d22afde45a [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
17
18class ParenMatch:
19 """Highlight matching parentheses
20
21 There are three supported style of paren matching, based loosely
22 on the Emacs options. The style is select based on the
23 HILITE_STYLE attribute; it can be changed used the set_style
24 method.
25
26 The supported styles are:
27
28 default -- When a right paren is typed, highlight the matching
29 left paren for 1/2 sec.
30
31 expression -- When a right paren is typed, highlight the entire
32 expression from the left paren to the right paren.
33
34 TODO:
35 - fix interaction with CallTips
36 - extend IDLE with configuration dialog to change options
37 - implement rest of Emacs highlight styles (see below)
38 - print mismatch warning in IDLE status window
39
40 Note: In Emacs, there are several styles of highlight where the
41 matching paren is highlighted whenever the cursor is immediately
42 to the right of a right paren. I don't know how to do that in Tk,
43 so I haven't bothered.
44 """
45
46 menudefs = []
47
48 keydefs = {
49 '<<flash-open-paren>>' : ('<KeyRelease-parenright>',
50 '<KeyRelease-bracketright>',
51 '<KeyRelease-braceright>'),
52 '<<check-restore>>' : ('<KeyPress>',),
53 }
54
55 windows_keydefs = {}
56 unix_keydefs = {}
57
58 STYLE = "default" # or "expression"
59 FLASH_DELAY = 500
60 HILITE_CONFIG = {"foreground": "black",
61 "background": "#43cd80", # SeaGreen3
62 }
63 BELL = 1 # set to false for no bell
64
65 def __init__(self, editwin):
66 self.editwin = editwin
67 self.text = editwin.text
68 self.finder = LastOpenBracketFinder(editwin)
69 self.counter = 0
70 self._restore = None
71 self.set_style(self.STYLE)
72
73 def set_style(self, style):
74 self.STYLE = style
75 if style == "default":
76 self.create_tag = self.create_tag_default
77 self.set_timeout = self.set_timeout_last
78 elif style == "expression":
79 self.create_tag = self.create_tag_expression
80 self.set_timeout = self.set_timeout_none
81
82 def flash_open_paren_event(self, event):
83 index = self.finder.find(keysym_type(event.keysym))
84 if index is None:
85 self.warn_mismatched()
86 return
87 self._restore = 1
88 self.create_tag(index)
89 self.set_timeout()
90
91 def check_restore_event(self, event=None):
92 if self._restore:
93 self.text.tag_delete("paren")
94 self._restore = None
95
96 def handle_restore_timer(self, timer_count):
97 if timer_count + 1 == self.counter:
98 self.check_restore_event()
99
100 def warn_mismatched(self):
101 if self.BELL:
102 self.text.bell()
103
104 # any one of the create_tag_XXX methods can be used depending on
105 # the style
106
107 def create_tag_default(self, index):
108 """Highlight the single paren that matches"""
109 self.text.tag_add("paren", index)
110 self.text.tag_config("paren", self.HILITE_CONFIG)
111
112 def create_tag_expression(self, index):
113 """Highlight the entire expression"""
114 self.text.tag_add("paren", index, "insert")
115 self.text.tag_config("paren", self.HILITE_CONFIG)
116
117 # any one of the set_timeout_XXX methods can be used depending on
118 # the style
119
120 def set_timeout_none(self):
121 """Highlight will remain until user input turns it off"""
122 pass
123
124 def set_timeout_last(self):
125 """The last highlight created will be removed after .5 sec"""
126 # associate a counter with an event; only disable the "paren"
127 # tag if the event is for the most recent timer.
128 self.editwin.text_frame.after(self.FLASH_DELAY,
129 lambda self=self, c=self.counter: \
130 self.handle_restore_timer(c))
131 self.counter = self.counter + 1
132
133def keysym_type(ks):
134 # Not all possible chars or keysyms are checked because of the
135 # limited context in which the function is used.
136 if ks == "parenright" or ks == "(":
137 return "paren"
138 if ks == "bracketright" or ks == "[":
139 return "bracket"
140 if ks == "braceright" or ks == "{":
141 return "brace"
142
143class LastOpenBracketFinder:
144 num_context_lines = AutoIndent.num_context_lines
145 indentwidth = AutoIndent.indentwidth
146 tabwidth = AutoIndent.tabwidth
147 context_use_ps1 = AutoIndent.context_use_ps1
148
149 def __init__(self, editwin):
150 self.editwin = editwin
151 self.text = editwin.text
152
153 def _find_offset_in_buf(self, lno):
154 y = PyParse.Parser(self.indentwidth, self.tabwidth)
155 for context in self.num_context_lines:
156 startat = max(lno - context, 1)
157 startatindex = `startat` + ".0"
158 # rawtext needs to contain everything up to the last
159 # character, which was the close paren. also need to
160 # append "\n" to please the parser, which seems to expect
161 # a complete line
162 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)
177 if i is None:
178 return i
179 if keysym_type(buf[i]) != right_keysym_type:
180 return None
181 lines_back = buf[i:].count("\n") - 1
182 # subtract one for the "\n" added to please the parser
183 upto_open = buf[:i]
184 j = upto_open.rfind("\n") + 1 # offset of column 0 of line
185 offset = i - j
186 return "%d.%d" % (lno - lines_back, offset)
187
188 def _build_char_in_string_func(self, startindex):
189 def inner(offset, startindex=startindex,
190 icis=self.editwin.is_char_in_string):
191 return icis(startindex + "%dc" % offset)
192 return inner
193