179 lines
6.7 KiB
Python
179 lines
6.7 KiB
Python
"""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 PyParse
|
|
from EditorWindow import EditorWindow, index2line
|
|
from configHandler import idleConf
|
|
|
|
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 = []
|
|
STYLE = idleConf.GetOption('extensions','ParenMatch','style',
|
|
default='expression')
|
|
FLASH_DELAY = idleConf.GetOption('extensions','ParenMatch','flash-delay',
|
|
type='int',default=500)
|
|
HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(),'hilite')
|
|
BELL = idleConf.GetOption('extensions','ParenMatch','bell',
|
|
type='bool',default=1)
|
|
|
|
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 = EditorWindow.num_context_lines
|
|
indentwidth = EditorWindow.indentwidth
|
|
tabwidth = EditorWindow.tabwidth
|
|
context_use_ps1 = EditorWindow.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 = repr(startat) + ".0"
|
|
# rawtext needs to contain everything up to the last
|
|
# character, which was the close paren. the parser also
|
|
# requires that the last line ends with "\n"
|
|
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 \
|
|
or 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
|