summaryrefslogtreecommitdiffstats
path: root/Tools/idle/ParenMatch.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/idle/ParenMatch.py')
-rw-r--r--Tools/idle/ParenMatch.py193
1 files changed, 193 insertions, 0 deletions
diff --git a/Tools/idle/ParenMatch.py b/Tools/idle/ParenMatch.py
new file mode 100644
index 0000000..7500603
--- /dev/null
+++ b/Tools/idle/ParenMatch.py
@@ -0,0 +1,193 @@
+"""ParenMatch -- An IDLE extension for parenthesis matching.
+
+When you hit a right paren, the cursor should move briefly to the left
+paren. Paren here is used generically; the matching applies to
+parentheses, square brackets, and curly braces.
+
+WARNING: This extension will fight with the CallTips extension,
+because they both are interested in the KeyRelease-parenright event.
+We'll have to fix IDLE to do something reasonable when two or more
+extensions what to capture the same event.
+"""
+
+import string
+
+import PyParse
+from AutoIndent import AutoIndent, index2line
+
+class ParenMatch:
+ """Highlight matching parentheses
+
+ There are three supported style of paren matching, based loosely
+ on the Emacs options. The style is select based on the
+ HILITE_STYLE attribute; it can be changed used the set_style
+ method.
+
+ The supported styles are:
+
+ default -- When a right paren is typed, highlight the matching
+ left paren for 1/2 sec.
+
+ expression -- When a right paren is typed, highlight the entire
+ expression from the left paren to the right paren.
+
+ TODO:
+ - fix interaction with CallTips
+ - extend IDLE with configuration dialog to change options
+ - implement rest of Emacs highlight styles (see below)
+ - print mismatch warning in IDLE status window
+
+ Note: In Emacs, there are several styles of highlight where the
+ matching paren is highlighted whenever the cursor is immediately
+ to the right of a right paren. I don't know how to do that in Tk,
+ so I haven't bothered.
+ """
+
+ menudefs = []
+
+ keydefs = {
+ '<<flash-open-paren>>' : ('<KeyRelease-parenright>',
+ '<KeyRelease-bracketright>',
+ '<KeyRelease-braceright>'),
+ '<<check-restore>>' : ('<KeyPress>',),
+ }
+
+ windows_keydefs = {}
+ unix_keydefs = {}
+
+ STYLE = "default" # or "expression"
+ FLASH_DELAY = 500
+ HILITE_CONFIG = {"foreground": "black",
+ "background": "#43cd80", # SeaGreen3
+ }
+ BELL = 1 # set to false for no bell
+
+ def __init__(self, editwin):
+ self.editwin = editwin
+ self.text = editwin.text
+ self.finder = LastOpenBracketFinder(editwin)
+ self.counter = 0
+ self._restore = None
+ self.set_style(self.STYLE)
+
+ def set_style(self, style):
+ self.STYLE = style
+ if style == "default":
+ self.create_tag = self.create_tag_default
+ self.set_timeout = self.set_timeout_last
+ elif style == "expression":
+ self.create_tag = self.create_tag_expression
+ self.set_timeout = self.set_timeout_none
+
+ def flash_open_paren_event(self, event):
+ index = self.finder.find(keysym_type(event.keysym))
+ if index is None:
+ self.warn_mismatched()
+ return
+ self._restore = 1
+ self.create_tag(index)
+ self.set_timeout()
+
+ def check_restore_event(self, event=None):
+ if self._restore:
+ self.text.tag_delete("paren")
+ self._restore = None
+
+ def handle_restore_timer(self, timer_count):
+ if timer_count + 1 == self.counter:
+ self.check_restore_event()
+
+ def warn_mismatched(self):
+ if self.BELL:
+ self.text.bell()
+
+ # any one of the create_tag_XXX methods can be used depending on
+ # the style
+
+ def create_tag_default(self, index):
+ """Highlight the single paren that matches"""
+ self.text.tag_add("paren", index)
+ self.text.tag_config("paren", self.HILITE_CONFIG)
+
+ def create_tag_expression(self, index):
+ """Highlight the entire expression"""
+ self.text.tag_add("paren", index, "insert")
+ self.text.tag_config("paren", self.HILITE_CONFIG)
+
+ # any one of the set_timeout_XXX methods can be used depending on
+ # the style
+
+ def set_timeout_none(self):
+ """Highlight will remain until user input turns it off"""
+ pass
+
+ def set_timeout_last(self):
+ """The last highlight created will be removed after .5 sec"""
+ # associate a counter with an event; only disable the "paren"
+ # tag if the event is for the most recent timer.
+ self.editwin.text_frame.after(self.FLASH_DELAY,
+ lambda self=self, c=self.counter: \
+ self.handle_restore_timer(c))
+ self.counter = self.counter + 1
+
+def keysym_type(ks):
+ # Not all possible chars or keysyms are checked because of the
+ # limited context in which the function is used.
+ if ks == "parenright" or ks == "(":
+ return "paren"
+ if ks == "bracketright" or ks == "[":
+ return "bracket"
+ if ks == "braceright" or ks == "{":
+ return "brace"
+
+class LastOpenBracketFinder:
+ num_context_lines = AutoIndent.num_context_lines
+ indentwidth = AutoIndent.indentwidth
+ tabwidth = AutoIndent.tabwidth
+ context_use_ps1 = AutoIndent.context_use_ps1
+
+ def __init__(self, editwin):
+ self.editwin = editwin
+ self.text = editwin.text
+
+ def _find_offset_in_buf(self, lno):
+ y = PyParse.Parser(self.indentwidth, self.tabwidth)
+ for context in self.num_context_lines:
+ startat = max(lno - context, 1)
+ startatindex = `startat` + ".0"
+ # rawtext needs to contain everything up to the last
+ # character, which was the close paren. also need to
+ # append "\n" to please the parser, which seems to expect
+ # a complete line
+ rawtext = self.text.get(startatindex, "insert")[:-1] + "\n"
+ y.set_str(rawtext)
+ bod = y.find_good_parse_start(
+ self.context_use_ps1,
+ self._build_char_in_string_func(startatindex))
+ if bod is not None or startat == 1:
+ break
+ y.set_lo(bod or 0)
+ i = y.get_last_open_bracket_pos()
+ return i, y.str
+
+ def find(self, right_keysym_type):
+ """Return the location of the last open paren"""
+ lno = index2line(self.text.index("insert"))
+ i, buf = self._find_offset_in_buf(lno)
+ if i is None:
+ return i
+ if keysym_type(buf[i]) != right_keysym_type:
+ return None
+ lines_back = buf[i:].count("\n") - 1
+ # subtract one for the "\n" added to please the parser
+ upto_open = buf[:i]
+ j = upto_open.rfind("\n") + 1 # offset of column 0 of line
+ offset = i - j
+ return "%d.%d" % (lno - lines_back, offset)
+
+ def _build_char_in_string_func(self, startindex):
+ def inner(offset, startindex=startindex,
+ icis=self.editwin.is_char_in_string):
+ return icis(startindex + "%dc" % offset)
+ return inner
+