doc/edit/ now holds a shared Python implementation of the language's structural layer (klammertext_edit.py) and a dependency-free language server (klammertext_ls.py), with integrations for Emacs, Sublime Text, Vim, and Visual Studio Code. The editor test suite in tst/ covers the core's API and CLI, the language server protocol, the VS Code extension, headless Vim, and Emacs byte-equality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
7.0 KiB
Python
182 lines
7.0 KiB
Python
# Klammertext.py
|
|
#
|
|
# Sublime Text plugin for klammer APPLICATION (@) delimiters. Two features,
|
|
# both counterparts of doc/edit/emacs/klammertext-mode.el, both reusing the
|
|
# one context-sensitive matcher in the shared core:
|
|
#
|
|
# 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). 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.
|
|
#
|
|
# The matcher itself — on-or-just-after caret rule, literal klammers matched
|
|
# BY NAME with opaque content (@code <-> code@), everything else by depth,
|
|
# @@/@@@ runs and removed text stepped over — lives in the shared core,
|
|
# doc/edit/shared/klammertext_edit.py, together with the LITERAL_KLAMMERS
|
|
# policy list. This file is only the Sublime wrapper.
|
|
#
|
|
# The shared core is located next to this file (a vendored copy — the
|
|
# installed-package layout produced by doc/make_editing_zip.sh), or in
|
|
# ../shared (the repository layout), or under $KLAMMERTEXT_HOME.
|
|
#
|
|
# SYNC: the literal-klammer set in the shared core must agree with the @code
|
|
# rule + literal_code context in Klammertext.sublime-syntax (a static syntax
|
|
# file cannot read Python; both are seeded with just 'code').
|
|
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
import sublime
|
|
import sublime_plugin
|
|
_IN_SUBLIME = True
|
|
except ImportError: # standalone import outside Sublime Text
|
|
_IN_SUBLIME = False
|
|
|
|
|
|
def _import_shared():
|
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
candidates = [here, os.path.join(os.path.dirname(here), 'shared')]
|
|
kh = os.environ.get('KLAMMERTEXT_HOME')
|
|
if kh:
|
|
candidates.append(os.path.join(kh, 'doc', 'edit', 'shared'))
|
|
for d in candidates:
|
|
if os.path.isfile(os.path.join(d, 'klammertext_edit.py')):
|
|
if d not in sys.path:
|
|
sys.path.insert(0, d)
|
|
break
|
|
import klammertext_edit
|
|
return klammertext_edit
|
|
|
|
|
|
KE = _import_shared()
|
|
|
|
|
|
if _IN_SUBLIME:
|
|
|
|
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():
|
|
m = KE.match_at(s, region.b)
|
|
if m is None:
|
|
new_regions.append(region)
|
|
message = ("point is not on a klammer application "
|
|
"delimiter (@)")
|
|
continue
|
|
if m['match'] is None:
|
|
new_regions.append(region)
|
|
message = ("no matching delimiter for this %s klammer"
|
|
% ("opening" if m['kind'] == 'open'
|
|
else "closing"))
|
|
continue
|
|
new_regions.append(sublime.Region(m['match'], m['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 shared 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 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
|
|
s = self._buffer()
|
|
m = KE.match_at(s, sel[0].b)
|
|
if m is None:
|
|
self._clear()
|
|
return
|
|
|
|
regions = [sublime.Region(*m['token'])]
|
|
if m['match_token'] is not None:
|
|
regions.append(sublime.Region(*m['match_token']))
|
|
|
|
flags = sublime.DRAW_NO_FILL
|
|
if m['mismatch']:
|
|
view.erase_regions(self.MATCH_KEY)
|
|
view.add_regions(self.MISMATCH_KEY, regions,
|
|
'region.redish', '', flags)
|
|
if m['message']:
|
|
sublime.status_message("Klammertext: " + m['message'])
|
|
else:
|
|
view.erase_regions(self.MISMATCH_KEY)
|
|
view.add_regions(self.MATCH_KEY, regions,
|
|
'region.bluish', '', flags)
|