Editor indentation for Emacs and Sublime Text; container guides point to editor support (from dev 5d35f256476e)

This commit is contained in:
2026-07-27 00:49:08 +02:00
parent fd9a370af7
commit ac0e875fa8
10 changed files with 646 additions and 17 deletions

View File

@@ -4,8 +4,13 @@
// command) to Ctrl+M — Sublime's own "go to matching bracket" key, repurposed
// for klammers, since the built-in cannot match context-dependent @ pairs.
//
// The "selector" context confines the binding to Klammertext files, so Ctrl+M
// keeps its normal meaning everywhere else.
// Binds "reindent lines" (the companion Klammertext_indent.py command) to
// Ctrl+Alt+I. Sublime's own Reindent (Edit > Line > Reindent, unbound by
// default) is driven by single-line regex patterns that cannot express
// Klammertext nesting, so reindentation is a plugin command here too.
//
// The "selector" contexts confine the bindings to Klammertext files, so both
// keys keep their normal meaning everywhere else.
//
// macOS users may prefer "super+m"; change the "keys" value below. This file
// (no platform suffix) is loaded on all platforms.
@@ -16,5 +21,12 @@
"context": [
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
]
},
{
"keys": ["ctrl+alt+i"],
"command": "klammertext_reindent",
"context": [
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
]
}
]

View File

@@ -52,13 +52,15 @@ import sublime_plugin
# Klammer names whose content is a literal argument (verbatim interior).
#
# SYNC: this list is one of three copies that must agree. When you add or
# remove a literal klammer, mirror it in all three:
# SYNC: this list is one of four copies that must agree. When you add or
# remove a literal klammer, mirror it in all four:
# * 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
# * LITERAL_KLAMMERS in Klammertext_indent.py (a deletable unit, so it does
# not import from this file)
# * the @NAME literal rule + literal_NAME context in Klammertext.sublime-syntax
# All three are currently seeded with just "code".
# All four are currently seeded with just "code".
LITERAL_KLAMMERS = set(["code"])

View File

@@ -11,7 +11,8 @@ Emacs mode closely; where the two intentionally differ, the file headers say so.
|------|---------|
| `Klammertext.sublime-syntax` | Syntax highlighting. Colors the text-removal constructs (`#`, `##`, `#[...]#`) and the three `@`-tiers — application `@`, definition `@@`, system `@@@` — each as an opening vs. a close, plus `^`-escapes and verbatim `@code ... code@` spans. |
| `Klammertext.py` | Plugin with two features that share one context-sensitive matcher: jump between an opening and its close, and live highlighting of the matching delimiter as the caret moves (mismatched or unbalanced delimiters flag in red). |
| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M**, scoped to Klammertext files. |
| `Klammertext_indent.py` | **Experimental.** Reindentation per the Klammertext convention (see below). A separate unit: delete this one file to disable indentation; nothing else is affected. |
| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M** and reindent to **Ctrl+Alt+I**, scoped to Klammertext files. |
| `Comments.tmPreferences` | Comment toggling: **Ctrl+/** inserts `# ` (line removal), **Ctrl+Shift+/** wraps in `#[ ... ]#` (block removal). |
| `Breakers` / `Celeste` / `Mariana` / `Monokai` / `Sixteen` `.sublime-color-scheme` | Color overrides for Sublime's five built-in schemes — one hue system, full intensity on the dark schemes, scaled down on the light ones. Additive: they recolor only the Klammertext delimiters and leave the rest of each scheme unchanged. |
| `Klammertext_in_Sublime_Text.md` | This file. |
@@ -48,11 +49,48 @@ adaptive `region.*` scopes, which were added in ST4.
| caret on a klammer `@` | The matching delimiter boxes automatically; a name mismatch or unbalanced delimiter boxes in red with a status-bar message (equivalent of `show-paren-mode`) |
| **Ctrl+/** | Toggle line comment (`#`) |
| **Ctrl+Shift+/** | Toggle block comment (`#[ ... ]#`) |
| **Ctrl+Alt+I** | Reindent the selected lines (the current line when there is just a caret) — experimental, see "Indentation" below |
Ctrl+M is Sublime's own "go to matching bracket" key, reused here because the
built-in cannot match Klammertext's context-dependent `@`. macOS users who
prefer `super+m` can change it in `Default.sublime-keymap`.
## Indentation (experimental)
`Klammertext_indent.py` ports the Emacs mode's indentation
(`doc/emacs/klammertext-indent.el`): **Ctrl+Alt+I** reindents the line(s)
touched by the selection to reflect the klammer nesting, two spaces per level:
```
@ol
Item one
| Item two
@ol
Embedded item one
| Embedded item two
@
| Item three
@
```
The rule: a line indents to 2 × depth; a line *beginning* with a bar run
(`|`, `||`, …) or a closing delimiter sits one level less — at its owner's
opening column, so bars and closes line up under the `@` of the klammer they
belong to. All three `@`-tiers indent uniformly. Exceptions: `@document`
contributes no level (a document's paragraphs stay at the left margin); lines
inside verbatim `@code` content, inside `@eval` argument spans (inline Python
is indentation-sensitive), and inside removed regions are never touched. The
policy lists (`TRANSPARENT_KLAMMERS`, `CODE_KLAMMERS`, `INDENT_OFFSET`) are at
the top of `Klammertext_indent.py`, kept in sync with the Emacs defcustoms.
Sublime's own Reindent (Edit → Line → Reindent) is driven by single-line
regex patterns that cannot express Klammertext nesting, so this is a plugin
command instead. Nothing reformats automatically (no on-Enter auto-indent):
whitespace is content in Klammertext, so indentation happens only when you
ask for it. To disable the feature, delete `Klammertext_indent.py` and the
Ctrl+Alt+I entry in `Default.sublime-keymap` (or just the keymap entry, to
keep the command available from plugins).
## Colors
Colors are installed automatically for all five of Sublime's built-in schemes.
@@ -75,16 +113,17 @@ exact values are in each file's header comment.
## Keeping literal klammers in sync
Klammers whose content is verbatim (`@code ... code@`) are listed in three
Klammers whose content is verbatim (`@code ... code@`) are listed in four
places that must agree — a Sublime syntax/plugin cannot read the Emacs
defcustom, so the list is duplicated:
- `klammertext-literal-klammers` in `doc/emacs/klammertext-mode.el` (the source of truth)
- `LITERAL_KLAMMERS` in `Klammertext.py`
- `LITERAL_KLAMMERS` in `Klammertext_indent.py`
- the `@code` rule and `literal_code` context in `Klammertext.sublime-syntax`
All three are seeded with just `code`. When you add or remove a literal
klammer, change all three.
All four are seeded with just `code`. When you add or remove a literal
klammer, change all four.
## Not included

View File

@@ -0,0 +1,276 @@
# Klammertext_indent.py
#
# EXPERIMENTAL. Reindentation for Klammertext files — the Sublime Text port
# of doc/emacs/klammertext-indent.el. This file is a separate unit: delete it
# (or move it out of the package folder) to disable indentation entirely; the
# rest of the Klammertext package is unaffected.
#
# Command name (for keymaps / the command palette): klammertext_reindent
# Keybinding: Ctrl+Alt+I (in Default.sublime-keymap), scoped to Klammertext
# files. It reindents the line(s) touched by the selection — the current
# line when there is just a caret. Nothing reformats automatically (no
# on-Enter auto-indent), because whitespace is content in Klammertext.
#
# The convention (2026-07-27):
#
# @ol <- opener at its context's content column
# Item one <- content: opener column + 2
# | Item two <- bar run at the OPENER's column ("| " is two
# @ol characters, so item text aligns with "Item one")
# Embedded item one
# | Embedded item two
# @ <- close at its opener's column
# | Item four
# @
#
# Formal rule: a line indents to offset x (effective depth); a line that
# BEGINS with a bar run (|, ||, ...) or a closing delimiter (a bare @-run or
# a named close) indents one level less, i.e. to its owner's opening column.
# The bar-run rule is dimension-independent: | (list items), || (table rows)
# and any longer run all drop to the opener's column. Effective depth counts
# every enclosing span uniformly -- applications (@), definitions (@@), and
# system commands (@@@) -- with these exceptions:
#
# * Klammers in TRANSPARENT_KLAMMERS (seeded with "document") contribute no
# level, so ordinary paragraphs of a document sit at the left margin.
# * Lines inside a literal klammer's verbatim content (@code ... code@) and
# inside the argument span of a klammer in CODE_KLAMMERS (seeded with
# "eval" -- inline Python is indentation-sensitive!) are NEVER touched.
# Removed regions (#[ ... ]#, everything after ##) are likewise left
# alone.
#
# Known limitation (shared with the Emacs scanner): a raw @ inside a ^'...'^
# literal region would confuse the depth scan.
#
# SYNC: the policy lists below must agree with the Emacs side:
# * LITERAL_KLAMMERS with klammertext-literal-klammers (also duplicated in
# Klammertext.py and Klammertext.sublime-syntax; all seeded "code")
# * TRANSPARENT_KLAMMERS with klammertext-transparent-klammers ("document")
# * CODE_KLAMMERS with klammertext-code-klammers ("eval")
# * INDENT_OFFSET with klammertext-indent-offset (2)
# The scanning helpers (name_char_p, escaped_p, block_end, at_run_end) are
# duplicated from Klammertext.py so this file stays a deletable unit with no
# import coupling.
try:
import sublime
import sublime_plugin
_IN_SUBLIME = True
except ImportError: # standalone testing outside Sublime Text
_IN_SUBLIME = False
INDENT_OFFSET = 2
LITERAL_KLAMMERS = set(["code"])
TRANSPARENT_KLAMMERS = set(["document"])
CODE_KLAMMERS = set(["eval"])
# --- pure helpers (duplicated from Klammertext.py; see SYNC note above) -----
def name_char_p(ch):
"""True if CH can be part of a klammer name (letter, digit or _)."""
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."""
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
# --- the depth scanner (port of klammertext-indent--state-at) ---------------
def state_at(s, pos):
"""Scan s[0:POS] (POS a line beginning). Return (stack, opaque): STACK is
the list of names of the klammer applications, @@ definitions and @@@
commands open at POS, outermost first; OPAQUE is True when POS lies inside
content that indentation must not touch (removed text, a literal klammer's
verbatim span, or a code klammer's argument span)."""
stack = []
n = len(s)
i = 0
while i < pos:
j = i
while j < pos and s[j] != '@' and s[j] != '#':
j += 1
if j >= pos:
break
hit = j
i = hit + 1
if escaped_p(s, hit): # ^@ / ^# : plain text
continue
nxt = s[hit + 1] if hit + 1 < n else None
if s[hit] == '#':
if nxt == '#': # ## removes to end of buffer
return (stack, True)
elif nxt == '[': # #[ ... ]# (nestable)
end = block_end(s, hit + 2)
if pos < end:
return (stack, True)
i = end
elif nxt in ('+', '/', '-'): # whitespace operators
pass
else: # # to end of line
eol = s.find('\n', hit)
i = n if eol == -1 else eol
continue
# an @-run
run_end = at_run_end(s, hit)
run_len = run_end - hit
after = s[run_end] if run_end < n else None
if name_char_p(after):
# @name / @@name / @@@name : an opener (or, for a literal
# klammer, a verbatim span to step over).
k = run_end
while k < n and name_char_p(s[k]):
k += 1
name = s[run_end:k]
i = k
if run_len == 1 and name in LITERAL_KLAMMERS:
# Verbatim interior: find the closing NAME@ by name.
idx = s.find(name + '@', k)
if idx == -1: # never closed
return (stack, True)
close_end = idx + len(name) + 1
if pos < close_end:
return (stack, True)
i = close_end
elif run_len == 1 and k < n and s[k] == '-':
pass # @name-arg : opens no span
else:
stack.append(name)
else:
# a bare @-run, or the run of a named close: a close.
if stack:
stack.pop()
i = run_end
opaque = any(name in CODE_KLAMMERS for name in stack)
return (stack, opaque)
def depth(stack):
"""Number of indentation levels STACK contributes.
Transparent klammers contribute none."""
return sum(1 for name in stack if name not in TRANSPARENT_KLAMMERS)
def dedent_line_p(s, bol):
"""True when the line starting at BOL begins with a token that sits at its
owner's opening column: a bar run (|, ||, ...), a bare close run (@, @@,
@@@), or a named close (name@, name@@, name@@@). A line beginning with an
opener (@name, @@name, @@@name) is content-level."""
n = len(s)
i = bol
while i < n and s[i] in ' \t':
i += 1
if i >= n:
return False
c = s[i]
if c == '|':
return True
if c == '@':
run_end = at_run_end(s, i)
return not name_char_p(s[run_end] if run_end < n else None)
if name_char_p(c):
# A named close: name chars followed by an @-run (an unescaped @ can
# only be a delimiter).
k = i
while k < n and name_char_p(s[k]):
k += 1
return k < n and s[k] == '@'
return False
def target_column(s, bol):
"""Column for the line starting at BOL, or None for lines that must not
be touched (verbatim, code, or removed content)."""
stack, opaque = state_at(s, bol)
if opaque:
return None
if dedent_line_p(s, bol) and stack:
stack = stack[:-1]
return INDENT_OFFSET * depth(stack)
def reindent_lines(s, bols):
"""Compute the edits that reindent the lines whose beginnings are BOLS.
Return a list of (start, end, replacement) triples over S, in ascending
order, replacing each line's leading whitespace; untouchable lines and
already-correct lines produce no edit. Pure function -- also used by the
standalone tests."""
n = len(s)
edits = []
for bol in bols:
tgt = target_column(s, bol)
if tgt is None:
continue
i = bol
while i < n and s[i] in ' \t':
i += 1
if s[bol:i] != ' ' * tgt:
edits.append((bol, i, ' ' * tgt))
return edits
# --- the command ------------------------------------------------------------
if _IN_SUBLIME:
class KlammertextReindentCommand(sublime_plugin.TextCommand):
"""Reindent the line(s) touched by the selection per the Klammertext
convention. Sublime equivalent of TAB in the Emacs mode (which has no
Sublime analogue: TAB there always inserts). Bound to Ctrl+Alt+I."""
def run(self, edit):
view = self.view
s = view.substr(sublime.Region(0, view.size()))
bols = []
seen = set()
for region in view.sel():
for line in view.lines(region):
if line.a not in seen:
seen.add(line.a)
bols.append(line.a)
# Compute all edits from the original text, then apply from the
# bottom up so earlier offsets stay valid.
for a, b, new in sorted(reindent_lines(s, bols), reverse=True):
view.replace(edit, sublime.Region(a, b), new)
def is_enabled(self):
return self.view.match_selector(0, "text.klammertext")