2026-07-18 18:48:23 +02:00
|
|
|
# Klammertext.py
|
|
|
|
|
#
|
|
|
|
|
# Sublime Text plugin for klammer APPLICATION (@) delimiters. Two features,
|
|
|
|
|
# both ports of doc/emacs/klammertext-mode.el, both reusing one matcher:
|
|
|
|
|
#
|
|
|
|
|
# 1. Jump between an opening and its close — the Sublime equivalent of the
|
|
|
|
|
# Emacs mode's `klammertext-jump-to-match' (bound C-c C-j). Command name
|
|
|
|
|
# klammertext_jump_to_match; keybinding in Default.sublime-keymap.
|
|
|
|
|
#
|
|
|
|
|
# 2. Live highlighting of the matching delimiter as the caret sits on one —
|
|
|
|
|
# the equivalent of the Emacs mode's show-paren support. Implemented as a
|
|
|
|
|
# ViewEventListener (see KlammertextMatchHighlighter at the bottom); no
|
|
|
|
|
# language server is involved. A mismatched named close or an unbalanced
|
|
|
|
|
# delimiter is highlighted in red with a status-bar message, mirroring the
|
|
|
|
|
# Emacs mode's klammertext-mismatch-face + minibuffer report.
|
|
|
|
|
#
|
|
|
|
|
# This is the companion to Klammertext.sublime-syntax. The syntax file only
|
|
|
|
|
# colors tokens; a tokenizer cannot match context-dependent delimiters, so the
|
|
|
|
|
# jump is implemented here as a TextCommand. The keybinding lives in the
|
|
|
|
|
# companion Default.sublime-keymap.
|
|
|
|
|
#
|
|
|
|
|
# Command name (for keymaps / the command palette): klammertext_jump_to_match
|
|
|
|
|
#
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# What it does (a direct port of the elisp matcher):
|
|
|
|
|
# * On an opening @name, move to its closing @ or name@.
|
|
|
|
|
# * On a close (bare @ or name@), move to the opening @name.
|
|
|
|
|
# * Triggers when the caret is ON the @ or immediately AFTER it (the same
|
|
|
|
|
# on-or-just-after rule the Emacs command uses).
|
|
|
|
|
# * Only single-@ APPLICATION delimiters match. @@/@@@ runs, removed text
|
|
|
|
|
# (#, ##, #[...]#), escaped ^@, and literal-klammer spans (@code ... code@)
|
|
|
|
|
# are stepped over, exactly as in the Emacs mode. The abbreviated
|
|
|
|
|
# @name-arg form opens no span.
|
|
|
|
|
# * Works at every caret when there are multiple selections.
|
|
|
|
|
#
|
|
|
|
|
# Literal klammers (identical to C-c C-j): a @code ... code@ span is opaque.
|
|
|
|
|
# The general depth scan still steps over such a span WHOLESALE when matching
|
|
|
|
|
# some OTHER klammer, so verbatim @ inside it never miscount. A literal
|
|
|
|
|
# klammer's OWN delimiters are matched BY NAME rather than by depth (see
|
|
|
|
|
# app_match): @code jumps to the next code@, and code@ to the nearest preceding
|
|
|
|
|
# @code — correct even when the content holds unbalanced @, e.g. @code x @ y
|
|
|
|
|
# code@. LITERAL_KLAMMERS lists these names; keep it in sync with the '@code'
|
|
|
|
|
# handling in Klammertext.sublime-syntax.
|
|
|
|
|
#
|
|
|
|
|
# LITERAL_KLAMMERS must stay in sync with the literal klammers recognized in
|
|
|
|
|
# Klammertext.sublime-syntax (seeded there as @code). The Emacs mode keeps this
|
|
|
|
|
# list in the `klammertext-literal-klammers' defcustom; a plugin has no access
|
|
|
|
|
# to it, so it is duplicated here.
|
|
|
|
|
|
|
|
|
|
import sublime
|
|
|
|
|
import sublime_plugin
|
|
|
|
|
|
|
|
|
|
# Klammer names whose content is a literal argument (verbatim interior).
|
|
|
|
|
#
|
2026-07-27 00:49:08 +02:00
|
|
|
# SYNC: this list is one of four copies that must agree. When you add or
|
|
|
|
|
# remove a literal klammer, mirror it in all four:
|
2026-07-18 18:48:23 +02:00
|
|
|
# * klammertext-literal-klammers in doc/emacs/klammertext-mode.el (the source
|
|
|
|
|
# of truth; a Sublime syntax/plugin cannot read that Emacs defcustom)
|
|
|
|
|
# * LITERAL_KLAMMERS here
|
2026-07-27 00:49:08 +02:00
|
|
|
# * LITERAL_KLAMMERS in Klammertext_indent.py (a deletable unit, so it does
|
|
|
|
|
# not import from this file)
|
2026-07-18 18:48:23 +02:00
|
|
|
# * the @NAME literal rule + literal_NAME context in Klammertext.sublime-syntax
|
2026-07-27 00:49:08 +02:00
|
|
|
# All four are currently seeded with just "code".
|
2026-07-18 18:48:23 +02:00
|
|
|
LITERAL_KLAMMERS = set(["code"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- pure helpers (operate on the whole buffer as a string) ----------------
|
|
|
|
|
|
|
|
|
|
def name_char_p(ch):
|
|
|
|
|
"""True if CH can be part of a klammer name (letter, digit or _).
|
|
|
|
|
A hyphen is NOT a name char: @name-arg1 ends the name at the first hyphen."""
|
|
|
|
|
if ch is None:
|
|
|
|
|
return False
|
|
|
|
|
return (('a' <= ch <= 'z') or ('A' <= ch <= 'Z')
|
|
|
|
|
or ('0' <= ch <= '9') or ch == '_')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def escaped_p(s, pos):
|
|
|
|
|
"""True if the char at POS is escaped by an odd run of ^ before it.
|
|
|
|
|
In Klammertext ^# and ^@ are literal, so such a char is not a delimiter."""
|
|
|
|
|
n = 0
|
|
|
|
|
i = pos - 1
|
|
|
|
|
while i >= 0 and s[i] == '^':
|
|
|
|
|
n += 1
|
|
|
|
|
i -= 1
|
|
|
|
|
return (n % 2) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def block_end(s, frm):
|
|
|
|
|
"""Index just after the ]# that closes a #[ block opened at FROM (the index
|
|
|
|
|
just after the opening #[). Counts nested #[ ... ]#; len(s) if unclosed."""
|
|
|
|
|
depth = 1
|
|
|
|
|
i = frm
|
|
|
|
|
n = len(s)
|
|
|
|
|
while depth > 0:
|
|
|
|
|
a = s.find('#[', i)
|
|
|
|
|
b = s.find(']#', i)
|
|
|
|
|
if a == -1 and b == -1:
|
|
|
|
|
return n
|
|
|
|
|
if b == -1 or (a != -1 and a < b):
|
|
|
|
|
depth += 1
|
|
|
|
|
i = a + 2
|
|
|
|
|
else:
|
|
|
|
|
depth -= 1
|
|
|
|
|
i = b + 2
|
|
|
|
|
return i
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def at_run_end(s, pos):
|
|
|
|
|
"""Index just after the run of @ that begins at POS."""
|
|
|
|
|
p = pos
|
|
|
|
|
n = len(s)
|
|
|
|
|
while p < n and s[p] == '@':
|
|
|
|
|
p += 1
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_app_delim(s, i, limit):
|
|
|
|
|
"""From index I, find the next single-@ application delimiter before LIMIT.
|
|
|
|
|
Step over @@/@@@ runs, removed text, literal spans, escaped ^@, and the
|
|
|
|
|
abbreviated @name-arg form. Return (pos, kind, next_i) with kind 'open' or
|
|
|
|
|
'close' and next_i the index to resume from, or None when none is found."""
|
|
|
|
|
n = len(s)
|
|
|
|
|
if limit is None:
|
|
|
|
|
limit = n
|
|
|
|
|
while i < limit:
|
|
|
|
|
# find next @ or # at or after i (emacs re-search-forward "[@#]")
|
|
|
|
|
j = i
|
|
|
|
|
while j < limit and s[j] != '@' and s[j] != '#':
|
|
|
|
|
j += 1
|
|
|
|
|
if j >= limit:
|
|
|
|
|
return None
|
|
|
|
|
hit = j
|
|
|
|
|
i = hit + 1 # default: advance past the hit
|
|
|
|
|
if escaped_p(s, hit): # ^@ / ^# : keep going
|
|
|
|
|
continue
|
|
|
|
|
nxt = s[hit + 1] if hit + 1 < n else None
|
|
|
|
|
if s[hit] == '#': # removal: step over it
|
|
|
|
|
if nxt == '#':
|
|
|
|
|
i = n
|
|
|
|
|
elif nxt == '[':
|
|
|
|
|
i = block_end(s, hit + 2)
|
|
|
|
|
elif nxt in ('+', '/', '-'):
|
|
|
|
|
i = hit + 1
|
|
|
|
|
else: # to end of line
|
|
|
|
|
eol = s.find('\n', hit)
|
|
|
|
|
i = n if eol == -1 else eol
|
|
|
|
|
continue
|
|
|
|
|
# s[hit] == '@'
|
|
|
|
|
if nxt == '@': # @@ / @@@ : step over the run
|
|
|
|
|
i = at_run_end(s, hit)
|
|
|
|
|
continue
|
|
|
|
|
if name_char_p(nxt): # @name : opening?
|
|
|
|
|
k = hit + 1
|
|
|
|
|
while k < n and name_char_p(s[k]):
|
|
|
|
|
k += 1
|
|
|
|
|
name = s[hit + 1:k]
|
|
|
|
|
after = s[k] if k < n else None
|
|
|
|
|
if name in LITERAL_KLAMMERS: # literal span: skip to its close
|
|
|
|
|
close = name + '@'
|
|
|
|
|
idx = s.find(close, k)
|
|
|
|
|
i = n if idx == -1 else idx + len(close)
|
|
|
|
|
continue
|
|
|
|
|
elif after == '-': # @name-arg : opens no span
|
|
|
|
|
i = k
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
return (hit, 'open', k)
|
|
|
|
|
else: # name@ / bare @ : closing
|
|
|
|
|
return (hit, 'close', hit + 1)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def match_forward(s, open_pos):
|
|
|
|
|
"""OPEN_POS is the @ of an opening application. Return the matching close @
|
|
|
|
|
index, or None if unbalanced."""
|
|
|
|
|
n = len(s)
|
|
|
|
|
i = open_pos + 1
|
|
|
|
|
while i < n and name_char_p(s[i]): # past the opening name
|
|
|
|
|
i += 1
|
|
|
|
|
depth = 1
|
|
|
|
|
while depth > 0:
|
|
|
|
|
d = next_app_delim(s, i, None)
|
|
|
|
|
if d is None:
|
|
|
|
|
return None
|
|
|
|
|
pos, kind, nxt = d
|
|
|
|
|
i = nxt
|
|
|
|
|
if kind == 'open':
|
|
|
|
|
depth += 1
|
|
|
|
|
else:
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0:
|
|
|
|
|
return pos
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def match_backward(s, close_pos):
|
|
|
|
|
"""CLOSE_POS is the @ of a closing application. Return the matching open @
|
|
|
|
|
index, or None if unbalanced. Scans forward from 0 with a stack."""
|
|
|
|
|
stack = []
|
|
|
|
|
i = 0
|
|
|
|
|
limit = close_pos + 1
|
|
|
|
|
while True:
|
|
|
|
|
d = next_app_delim(s, i, limit)
|
|
|
|
|
if d is None:
|
|
|
|
|
return None
|
|
|
|
|
pos, kind, nxt = d
|
|
|
|
|
i = nxt
|
|
|
|
|
if kind == 'open':
|
|
|
|
|
stack.append(pos)
|
|
|
|
|
else:
|
|
|
|
|
open_pos = stack.pop() if stack else None
|
|
|
|
|
if pos == close_pos:
|
|
|
|
|
return open_pos
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def app_delim_info(s, pos):
|
|
|
|
|
"""If the char at POS is a single-@ application delimiter, return
|
|
|
|
|
(pos, kind) with kind 'open' or 'close'; else None. The abbreviated
|
|
|
|
|
@name-arg form (which opens no span) returns None."""
|
|
|
|
|
n = len(s)
|
|
|
|
|
if not (0 <= pos < n):
|
|
|
|
|
return None
|
|
|
|
|
if s[pos] != '@':
|
|
|
|
|
return None
|
|
|
|
|
if pos > 0 and s[pos - 1] == '@':
|
|
|
|
|
return None
|
|
|
|
|
if pos + 1 < n and s[pos + 1] == '@':
|
|
|
|
|
return None
|
|
|
|
|
if escaped_p(s, pos):
|
|
|
|
|
return None
|
|
|
|
|
nxt = s[pos + 1] if pos + 1 < n else None
|
|
|
|
|
if name_char_p(nxt):
|
|
|
|
|
k = pos + 1
|
|
|
|
|
while k < n and name_char_p(s[k]):
|
|
|
|
|
k += 1
|
|
|
|
|
after = s[k] if k < n else None
|
|
|
|
|
if after == '-':
|
|
|
|
|
return None
|
|
|
|
|
return (pos, 'open')
|
|
|
|
|
return (pos, 'close')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- name / mismatch helpers (for the live highlighter) --------------------
|
|
|
|
|
|
|
|
|
|
def _name_forward(s, pos):
|
|
|
|
|
"""Index just past the run of name chars starting at POS."""
|
|
|
|
|
n = len(s)
|
|
|
|
|
k = pos
|
|
|
|
|
while k < n and name_char_p(s[k]):
|
|
|
|
|
k += 1
|
|
|
|
|
return k
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def open_name(s, open_pos):
|
|
|
|
|
"""Name of the opening @name whose @ is at OPEN_POS."""
|
|
|
|
|
return s[open_pos + 1:_name_forward(s, open_pos + 1)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def close_name(s, close_pos):
|
|
|
|
|
"""Name of a named close NAME@ whose @ is at CLOSE_POS, or None for a bare @
|
|
|
|
|
(including the compact @name@ form, whose name belongs to the opening)."""
|
|
|
|
|
ns = close_pos
|
|
|
|
|
while ns > 0 and name_char_p(s[ns - 1]):
|
|
|
|
|
ns -= 1
|
|
|
|
|
if ns < close_pos and (ns == 0 or s[ns - 1] != '@'):
|
|
|
|
|
return s[ns:close_pos]
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paren_mismatch(s, open_pos, close_pos):
|
|
|
|
|
"""True if the pair is unbalanced (either side None) or the named close
|
|
|
|
|
disagrees with the opening name."""
|
|
|
|
|
if open_pos is None or close_pos is None:
|
|
|
|
|
return True
|
|
|
|
|
cname = close_name(s, close_pos)
|
|
|
|
|
return cname is not None and cname != open_name(s, open_pos)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def token_region(s, pos, kind):
|
|
|
|
|
"""(start, end) of the whole delimiter token whose @ is at POS.
|
|
|
|
|
Opening: @ plus its name. Named close: the name plus @. Bare @: just @."""
|
|
|
|
|
if kind == 'open':
|
|
|
|
|
return (pos, _name_forward(s, pos + 1))
|
|
|
|
|
ns = pos
|
|
|
|
|
while ns > 0 and name_char_p(s[ns - 1]):
|
|
|
|
|
ns -= 1
|
|
|
|
|
if ns < pos and (ns == 0 or s[ns - 1] != '@'):
|
|
|
|
|
return (ns, pos + 1) # named close NAME@
|
|
|
|
|
return (pos, pos + 1) # bare @ (or @name@)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- matching dispatch: literal klammers by name, others by depth ----------
|
|
|
|
|
|
|
|
|
|
def literal_delim_name(s, pos, kind):
|
|
|
|
|
"""If the application delimiter at POS (kind 'open'/'close') belongs to a
|
|
|
|
|
literal klammer (name in LITERAL_KLAMMERS), return its name; else None.
|
|
|
|
|
A literal klammer's @NAME open and NAME@ close are matched by name, not by
|
|
|
|
|
depth counting, because its content is verbatim."""
|
|
|
|
|
name = open_name(s, pos) if kind == 'open' else close_name(s, pos)
|
|
|
|
|
if name and name in LITERAL_KLAMMERS:
|
|
|
|
|
return name
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def literal_match_forward(s, open_pos, name):
|
|
|
|
|
"""Index of the @ of the NAME@ that closes the literal @NAME at OPEN_POS, or
|
|
|
|
|
None. The content is opaque, so search for the literal close string."""
|
|
|
|
|
start = open_pos + 1 + len(name)
|
|
|
|
|
idx = s.find(name + '@', start)
|
|
|
|
|
return idx + len(name) if idx != -1 else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def literal_match_backward(s, close_pos, name):
|
|
|
|
|
"""Index of the @ of the @NAME that opens the literal NAME@ whose @ is at
|
|
|
|
|
CLOSE_POS, or None. Literal spans do not nest, so the nearest preceding
|
|
|
|
|
real @NAME is the opener (not @@NAME, and not escaped)."""
|
|
|
|
|
open_str = '@' + name
|
|
|
|
|
end = close_pos
|
|
|
|
|
while True:
|
|
|
|
|
idx = s.rfind(open_str, 0, end)
|
|
|
|
|
if idx == -1:
|
|
|
|
|
return None
|
|
|
|
|
before = s[idx - 1] if idx > 0 else None
|
|
|
|
|
if before != '@' and not escaped_p(s, idx):
|
|
|
|
|
return idx
|
|
|
|
|
end = idx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def app_match(s, pos, kind):
|
|
|
|
|
"""Matching application delimiter for the delimiter at POS of KIND
|
|
|
|
|
('open'/'close'), or None. A literal klammer matches by name (@NAME <->
|
|
|
|
|
NAME@) with content opaque; other klammers match by depth."""
|
|
|
|
|
lit = literal_delim_name(s, pos, kind)
|
|
|
|
|
if lit is not None:
|
|
|
|
|
return (literal_match_forward(s, pos, lit) if kind == 'open'
|
|
|
|
|
else literal_match_backward(s, pos, lit))
|
|
|
|
|
return match_forward(s, pos) if kind == 'open' else match_backward(s, pos)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- the command -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class KlammertextJumpToMatchCommand(sublime_plugin.TextCommand):
|
|
|
|
|
"""Jump between a klammer application's opening and closing delimiter.
|
|
|
|
|
Sublime equivalent of the Emacs mode's C-c C-j."""
|
|
|
|
|
|
|
|
|
|
def run(self, edit):
|
|
|
|
|
view = self.view
|
|
|
|
|
s = view.substr(sublime.Region(0, view.size()))
|
|
|
|
|
new_regions = []
|
|
|
|
|
moved = False
|
|
|
|
|
message = None
|
|
|
|
|
|
|
|
|
|
for region in view.sel():
|
|
|
|
|
p = region.b
|
|
|
|
|
info = app_delim_info(s, p)
|
|
|
|
|
if info is None and p > 0:
|
|
|
|
|
info = app_delim_info(s, p - 1)
|
|
|
|
|
if info is None:
|
|
|
|
|
new_regions.append(region)
|
|
|
|
|
message = "point is not on a klammer application delimiter (@)"
|
|
|
|
|
continue
|
|
|
|
|
dpos, kind = info
|
|
|
|
|
match = app_match(s, dpos, kind)
|
|
|
|
|
if match is None:
|
|
|
|
|
new_regions.append(region)
|
|
|
|
|
message = ("no matching delimiter for this %s klammer"
|
|
|
|
|
% ("opening" if kind == 'open' else "closing"))
|
|
|
|
|
continue
|
|
|
|
|
new_regions.append(sublime.Region(match, match))
|
|
|
|
|
moved = True
|
|
|
|
|
|
|
|
|
|
view.sel().clear()
|
|
|
|
|
for r in new_regions:
|
|
|
|
|
view.sel().add(r)
|
|
|
|
|
|
|
|
|
|
if moved:
|
|
|
|
|
view.show(view.sel()[0].b)
|
|
|
|
|
elif message:
|
|
|
|
|
sublime.status_message("Klammertext: " + message)
|
|
|
|
|
|
|
|
|
|
def is_enabled(self):
|
|
|
|
|
# Only meaningful in Klammertext buffers.
|
|
|
|
|
return self.view.match_selector(0, "text.klammertext")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- live matched-delimiter highlighting (show-paren equivalent) -----------
|
|
|
|
|
|
|
|
|
|
class KlammertextMatchHighlighter(sublime_plugin.ViewEventListener):
|
|
|
|
|
"""Highlight the matching klammer application delimiter as the caret sits
|
|
|
|
|
on one. The Sublime equivalent of the Emacs mode's show-paren support —
|
|
|
|
|
driven by cursor movement, reusing the same context-sensitive matcher.
|
|
|
|
|
|
|
|
|
|
A matched pair is boxed (region.bluish); a mismatch or unbalanced delimiter
|
|
|
|
|
is boxed in red (region.redish) with a status-bar message. Both the token
|
|
|
|
|
under the caret and its match are boxed; the Emacs mode highlights only the
|
|
|
|
|
single @ character, but boxing the whole @name / name@ reads better here.
|
|
|
|
|
To highlight only the far delimiter, drop the first region in _update()."""
|
|
|
|
|
|
|
|
|
|
MATCH_KEY = 'klammertext_paren_match'
|
|
|
|
|
MISMATCH_KEY = 'klammertext_paren_mismatch'
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def is_applicable(cls, settings):
|
|
|
|
|
return str(settings.get('syntax', '')).endswith('Klammertext.sublime-syntax')
|
|
|
|
|
|
|
|
|
|
def __init__(self, view):
|
|
|
|
|
super().__init__(view)
|
|
|
|
|
self._change_count = -1
|
|
|
|
|
self._text = ''
|
|
|
|
|
|
|
|
|
|
def _buffer(self):
|
|
|
|
|
# Re-read the buffer only when it has actually changed, so plain cursor
|
|
|
|
|
# movement over a large file does not re-copy the whole document.
|
|
|
|
|
cc = self.view.change_count()
|
|
|
|
|
if cc != self._change_count:
|
|
|
|
|
self._text = self.view.substr(sublime.Region(0, self.view.size()))
|
|
|
|
|
self._change_count = cc
|
|
|
|
|
return self._text
|
|
|
|
|
|
|
|
|
|
def on_selection_modified_async(self):
|
|
|
|
|
self._update()
|
|
|
|
|
|
|
|
|
|
def on_activated_async(self):
|
|
|
|
|
self._update()
|
|
|
|
|
|
|
|
|
|
def _clear(self):
|
|
|
|
|
self.view.erase_regions(self.MATCH_KEY)
|
|
|
|
|
self.view.erase_regions(self.MISMATCH_KEY)
|
|
|
|
|
|
|
|
|
|
def _update(self):
|
|
|
|
|
view = self.view
|
|
|
|
|
sel = view.sel()
|
|
|
|
|
if len(sel) == 0:
|
|
|
|
|
self._clear()
|
|
|
|
|
return
|
|
|
|
|
p = sel[0].b
|
|
|
|
|
s = self._buffer()
|
|
|
|
|
|
|
|
|
|
info = app_delim_info(s, p)
|
|
|
|
|
if info is None and p > 0:
|
|
|
|
|
info = app_delim_info(s, p - 1)
|
|
|
|
|
if info is None:
|
|
|
|
|
self._clear()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
dpos, kind = info
|
|
|
|
|
match = app_match(s, dpos, kind)
|
|
|
|
|
open_pos = dpos if kind == 'open' else match
|
|
|
|
|
close_pos = match if kind == 'open' else dpos
|
|
|
|
|
mism = paren_mismatch(s, open_pos, close_pos)
|
|
|
|
|
|
|
|
|
|
regions = [sublime.Region(*token_region(s, dpos, kind))]
|
|
|
|
|
if match is not None:
|
|
|
|
|
other_kind = 'close' if kind == 'open' else 'open'
|
|
|
|
|
regions.append(sublime.Region(*token_region(s, match, other_kind)))
|
|
|
|
|
|
|
|
|
|
flags = sublime.DRAW_NO_FILL
|
|
|
|
|
if mism:
|
|
|
|
|
view.erase_regions(self.MATCH_KEY)
|
|
|
|
|
view.add_regions(self.MISMATCH_KEY, regions, 'region.redish', '', flags)
|
|
|
|
|
if match is None:
|
|
|
|
|
if kind == 'open':
|
|
|
|
|
msg = "opening @%s has no matching close" % open_name(s, open_pos)
|
|
|
|
|
else:
|
|
|
|
|
msg = "closing delimiter has no matching open"
|
|
|
|
|
else:
|
|
|
|
|
msg = ("closing %s@ does not match opening @%s"
|
|
|
|
|
% (close_name(s, close_pos) or '?', open_name(s, open_pos)))
|
|
|
|
|
sublime.status_message("Klammertext: " + msg)
|
|
|
|
|
else:
|
|
|
|
|
view.erase_regions(self.MISMATCH_KEY)
|
|
|
|
|
view.add_regions(self.MATCH_KEY, regions, 'region.bluish', '', flags)
|