VS Code: decoration-based matching, Ctrl+K bindings, README overhaul (from dev f8451715e657)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 17:02:41 +02:00
parent 2eb16791d2
commit 6abb0fd3dd
10 changed files with 255 additions and 39 deletions

View File

@@ -283,6 +283,26 @@ class Server:
'kind': 1})
self.reply(msg_id, highlights)
def on_klammertext_matchInfo(self, msg_id, params):
"""Custom request: the full matching story for a cursor position —
token, matching token, and whether the pair mismatches. The VS Code
extension draws its live match/mismatch decorations from this
(documentHighlight is word-gated in VS Code, so a bare @ close would
never trigger it; and it cannot carry the mismatch flag)."""
uri = params['textDocument']['uri']
text = self.docs.get(uri, '')
m = KE.match_at(text, pos_to_offset(text, params['position']))
if m is None:
self.reply(msg_id, None)
return
self.reply(msg_id, {
'token': offsets_to_range(text, *m['token']),
'matchToken': (offsets_to_range(text, *m['match_token'])
if m['match_token'] is not None else None),
'mismatch': m['mismatch'],
'message': m['message'],
})
def on_textDocument_definition(self, msg_id, params):
uri = params['textDocument']['uri']
text = self.docs.get(uri, '')

View File

@@ -52,7 +52,23 @@ support built in, so there is no conflict out of the box; if you install a
Kotlin extension, the two will contend for `.kt` and you can decide per
file with the language-mode picker (or `files.associations`).
## What you get
## VS Code commands for Klammertext
| Command | Menu | Key | Cursor position |
| --- | --- | --- | --- |
| Format Document | Right-click | `Ctrl+Shift+I` | Anywhere in the document |
| Format Selection | Right-click | `Ctrl+K Ctrl+F` | Lines selected |
| Toggle Line Comment | Edit menu | `Ctrl+/` | In the line to remove with `#` |
| Toggle Block Comment | Edit menu | `Ctrl+Shift+A` | Region to remove selected (`#[ ... ]#`) |
| Klammertext: Jump to Matching Delimiter | Right-click | `Ctrl+K J` | On the opening `@name` or the closing `name@` / `@` |
| Klammertext: Align Table | Right-click | `Ctrl+K A` | Anywhere inside the `@table` |
| Delimiter diagnostics | Problems panel | — | Automatic, as you type |
All commands are also in the Command Palette (`Ctrl+Shift+P`). Keys shown
are the Linux defaults: the Klammertext commands use `Cmd+K` on macOS, and
the built-in formatting and comment keys differ per OS.
## Features
**Syntax highlighting** — the same token classes as the Emacs, Sublime
Text, and Vim support: text removal (`#`, `##`, nestable `#[ ... ]#`), the
@@ -65,42 +81,71 @@ Klammertext palette (application blue / definition green / system orange,
opens bright and closes darker), add `editor.tokenColorCustomizations`
rules for the `*.klammertext` scopes in your settings.
**Diagnostics** — unclosed and mismatched delimiters appear in the
Problems panel as you type.
**Formatting****Format Document** / **Format Selection** reindent
structurally (2 spaces per nesting level; bar runs and closing delimiters
sit at their opener's column; `@document` content stays at the margin;
verbatim `@code` interiors, `@eval` code, and removed text are never
touched). Reindentation is **explicit-only**: there is deliberately no
**Structural reindentation** — Format Document and Format Selection
reindent per the Klammertext convention: 2 spaces per nesting level; a
line beginning with a bar run or a closing delimiter sits at its opener's
column; `@document` content stays at the margin; verbatim `@code`
interiors, `@eval` code, and removed text are never touched.
Reindentation is **explicit-only**: there is deliberately no
format-on-type, because whitespace is content in Klammertext.
**Delimiter matching** — with the cursor on an application delimiter, the
matching delimiter highlights (occurrences highlighting); **Go to
Definition** on a delimiter goes to its match. Literal klammers match by
name (`@code``code@`) with their verbatim content opaque; everything
else matches by depth.
**Delimiter matching** — with the cursor on an application delimiter
(opening `@name`, named close `name@`, or a bare `@` close), the delimiter
and its match are boxed; a mismatched named close or an unbalanced
delimiter is boxed in **red** with a status-bar message, as in the Emacs,
Sublime, and Vim support. **Go to Definition** on a delimiter goes to its
match, so Jump to Matching Delimiter has a second home on `F12`. Literal
klammers match by name (`@code``code@`) with their verbatim content
opaque — a stray `@` in the verbatim interior cannot confuse them;
everything else matches by depth. Double-click selects a whole delimiter
token.
**Commands and keybindings** (when editing Klammertext):
**Delimiter diagnostics** — the automatic Problems-panel entries cover
all three `@`-tiers: a closing delimiter with no opening, a named close
that disagrees with its opening (`ul@` closing `@ol`), a close of the
wrong tier (`@@` closing `@name`), openings never closed, and unclosed
`@code` and `#[` regions.
| Key | Command |
|---|---|
| `Ctrl+Alt+J` (`Cmd+Alt+J`) | Klammertext: Jump to Matching Delimiter |
| `Ctrl+Alt+A` (`Cmd+Alt+A`) | Klammertext: Align Table |
**Table alignment** — pads the cells of the `@table` enclosing the cursor
so the `|` separators line up, with the rules shared across the editors:
rows end with `||`; a row with a cell over 30 characters or spanning lines
is left untouched; beyond 100 aligned columns the command declines; bars
inside a nested klammer belong to that klammer, not the table; and no
whitespace is ever inserted inside a bar run (`||` is a row separator,
`| |` an empty cell).
Table alignment pads the cells of the `@table` enclosing the cursor so the
`|` separators line up, with the shared rules: rows end with `||`; a row
with a cell over 30 characters or spanning lines is left untouched; beyond
100 columns the command declines; bars inside a nested klammer are not
separators; no whitespace is ever inserted inside a bar run.
**Text removal** — the Toggle Comment commands are VS Code's names; in
Klammertext they toggle `#` line removal and `#[ ... ]#` block removal
(the `#` does not "comment out": it removes text from processing).
**Text removal toggling**`Ctrl+/` toggles `#` line removal and
`Shift+Alt+A` wraps the selection in `#[ ... ]#`, via the standard VS Code
comment commands.
**Keybinding notes** — each Klammertext command has two bindings because
some environments never deliver `Ctrl+Alt+letter` chords to VS Code (a
right Alt is usually AltGr, not Alt, and some desktops and input methods
intercept the chord); the two-step `Ctrl+K` chords go through everywhere.
If a key seems to do nothing, run the command from the Command Palette
first: if that works, the chord is being intercepted — open **Keyboard
Shortcuts** (`Ctrl+K Ctrl+S`), search "klammertext", and rebind.
## Settings
A normal installation needs neither setting: the extension runs `python3`
from `PATH` and finds the language server automatically (the copy vendored
next to `extension.js`, then `../shared/`, then
`$KLAMMERTEXT_HOME/doc/edit/shared/`). They exist for unusual setups.
Set them in the Settings UI (`Ctrl+,`, search "klammertext") or in
`settings.json`; the server is spawned when the extension activates, so
reload the window after changing either.
| Setting | Meaning (default) |
|---|---|
| `klammertext.pythonPath` | Python interpreter for the server (`python3`) |
| `klammertext.serverPath` | full path to `klammertext_ls.py` (auto-located) |
`pythonPath` matters when `python3` is not on the `PATH` VS Code sees — a
VS Code launched from the desktop inherits a different environment than
your shell — or when a specific interpreter is wanted. `serverPath`
matters only when the server file lives outside the search chain above.
If the server cannot be started at all, the extension says so once at
activation; highlighting still works, and everything structural
(diagnostics, formatting, matching, alignment) waits until the path is
fixed.

View File

@@ -167,6 +167,11 @@ function activate(context) {
}
const python = vscode.workspace.getConfiguration('klammertext').get('pythonPath') || 'python3';
client = new LspClient(python, [serverPath], log);
client.proc.on('error', (err) => {
vscode.window.showWarningMessage(
'Klammertext: could not start the language server (' + err.message +
') — check the klammertext.pythonPath setting.');
});
const diagnostics = vscode.languages.createDiagnosticCollection('klammertext');
context.subscriptions.push(diagnostics, output);
@@ -271,6 +276,58 @@ function activate(context) {
},
}));
// -- live match/mismatch decorations --
// Drawn on every cursor move from the server's klammertext/matchInfo.
// Deliberately NOT left to occurrence highlighting: VS Code only asks
// documentHighlight providers when the cursor is on a word, so a bare @
// close would never light up — and a mismatch could not show in red.
const matchDecoration = vscode.window.createTextEditorDecorationType({
border: '1px solid',
borderColor: new vscode.ThemeColor('editorBracketMatch.border'),
backgroundColor: new vscode.ThemeColor('editorBracketMatch.background'),
});
const mismatchDecoration = vscode.window.createTextEditorDecorationType({
border: '1px solid #ff5555',
fontWeight: 'bold',
});
context.subscriptions.push(matchDecoration, mismatchDecoration);
const updateMatchDecorations = (editor) => {
if (!editor || !isKt(editor.document)) return;
client.request('klammertext/matchInfo',
Object.assign(docParams(editor.document),
{ position: fromVsPosition(editor.selection.active) }))
.then((info) => {
if (!info) {
editor.setDecorations(matchDecoration, []);
editor.setDecorations(mismatchDecoration, []);
return;
}
const ranges = [toVsRange(info.token)];
if (info.matchToken) ranges.push(toVsRange(info.matchToken));
if (info.mismatch) {
editor.setDecorations(matchDecoration, []);
editor.setDecorations(mismatchDecoration, ranges);
if (info.message) {
vscode.window.setStatusBarMessage(
'Klammertext: ' + info.message, 5000);
}
} else {
editor.setDecorations(mismatchDecoration, []);
editor.setDecorations(matchDecoration, ranges);
}
}, () => { /* server gone: leave decorations as they are */ });
};
let matchTimer = null;
context.subscriptions.push(
vscode.window.onDidChangeTextEditorSelection((event) => {
if (matchTimer) clearTimeout(matchTimer);
matchTimer = setTimeout(
() => updateMatchDecorations(event.textEditor), 50);
}),
vscode.window.onDidChangeActiveTextEditor(
(editor) => updateMatchDecorations(editor)));
// -- commands --
context.subscriptions.push(
vscode.commands.registerCommand('klammertext.jumpToMatch', () => {

View File

@@ -1,11 +1,18 @@
{
"comments": {
"lineComment": "#",
"blockComment": ["#[", "]#"]
"blockComment": [
"#[",
"]#"
]
},
"brackets": [
["#[", "]#"]
[
"#[",
"]#"
]
],
"autoClosingPairs": [],
"surroundingPairs": []
"surroundingPairs": [],
"wordPattern": "@{1,3}[A-Za-z0-9_]+|[A-Za-z0-9_]+@{1,3}|@{1,3}|[A-Za-z0-9_]+"
}

View File

@@ -2,7 +2,7 @@
"name": "klammertext",
"displayName": "Klammertext",
"description": "Klammertext language support: syntax highlighting, delimiter matching, structural reindentation, table alignment, and delimiter diagnostics.",
"version": "0.1.0",
"version": "0.1.2",
"publisher": "klammertext",
"license": "SEE LICENSE IN THE KLAMMERTEXT DISTRIBUTION",
"engines": {
@@ -53,12 +53,24 @@
}
],
"keybindings": [
{
"command": "klammertext.jumpToMatch",
"key": "ctrl+k j",
"mac": "cmd+k j",
"when": "editorTextFocus && editorLangId == klammertext"
},
{
"command": "klammertext.jumpToMatch",
"key": "ctrl+alt+j",
"mac": "cmd+alt+j",
"when": "editorTextFocus && editorLangId == klammertext"
},
{
"command": "klammertext.alignTable",
"key": "ctrl+k a",
"mac": "cmd+k a",
"when": "editorTextFocus && editorLangId == klammertext"
},
{
"command": "klammertext.alignTable",
"key": "ctrl+alt+a",
@@ -80,6 +92,20 @@
"description": "Full path to klammertext_ls.py. Leave blank to auto-locate: a copy next to the extension, ../shared/ relative to it (the Klammertext repository layout), or $KLAMMERTEXT_HOME/doc/edit/shared/."
}
}
},
"menus": {
"editor/context": [
{
"command": "klammertext.jumpToMatch",
"when": "editorLangId == klammertext",
"group": "1_modification@10"
},
{
"command": "klammertext.alignTable",
"when": "editorLangId == klammertext",
"group": "1_modification@11"
}
]
}
}
}