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