2026-07-27 00:49:08 +02:00
|
|
|
;;; klammertext-indent.el --- TAB indentation for Klammertext -*- lexical-binding: t; -*-
|
|
|
|
|
|
|
|
|
|
;; EXPERIMENTAL. This file is a separate unit, loaded from the init file:
|
|
|
|
|
;;
|
|
|
|
|
;; (require 'klammertext-indent)
|
|
|
|
|
;;
|
|
|
|
|
;; Comment that line out to disable indentation entirely; klammertext-mode
|
|
|
|
|
;; itself is untouched by this file.
|
|
|
|
|
;;
|
|
|
|
|
;; The convention (2026-07-26):
|
|
|
|
|
;;
|
|
|
|
|
;; @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 (@ or name@)
|
|
|
|
|
;; 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 `klammertext-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
|
|
|
|
|
;; `klammertext-code-klammers' (seeded with "eval" -- inline Python is
|
|
|
|
|
;; indentation-sensitive!) are NEVER touched: TAB returns `noindent'.
|
|
|
|
|
;; Removed regions (#[ ... ]#, everything after ##) are likewise left
|
|
|
|
|
;; alone.
|
|
|
|
|
;;
|
|
|
|
|
;; Indentation happens only on explicit TAB / indent-region; nothing
|
|
|
|
|
;; reformats automatically, because whitespace is content in Klammertext.
|
|
|
|
|
;; The convention is nevertheless semantically free where it applies: list
|
|
|
|
|
;; and table cell content is stripped by the SKS, leading whitespace
|
|
|
|
|
;; collapses in the html/tex targets, and a blank line that acquires
|
|
|
|
|
;; indentation spaces still separates paragraphs (the SKS paragraph
|
|
|
|
|
;; separator is \n *\n ).
|
|
|
|
|
;;
|
|
|
|
|
;; Known limitation: a raw @ inside a ^'...'^ literal region would confuse
|
|
|
|
|
;; the depth scan (the same limitation as the font-lock scanner).
|
|
|
|
|
;;
|
Editor support generalized: shared core, language server, Vim and VS Code (from dev eb5baf9cbe59)
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>
2026-07-27 15:01:49 +02:00
|
|
|
;; SYNC: the shared Python core doc/edit/shared/klammertext_edit.py — the
|
|
|
|
|
;; single implementation behind the Sublime, Vim, and VS Code integrations
|
|
|
|
|
;; and the language server — carries this file's policy as INDENT_OFFSET /
|
|
|
|
|
;; TRANSPARENT_KLAMMERS / CODE_KLAMMERS (an elisp defcustom cannot be read
|
|
|
|
|
;; from Python, so this unit remains an independent implementation, held
|
|
|
|
|
;; equal by tst/editor_test.sh's byte-equality checks). When you change
|
|
|
|
|
;; `klammertext-indent-offset', `klammertext-transparent-klammers' or
|
|
|
|
|
;; `klammertext-code-klammers', mirror the change there.
|
2026-07-27 00:49:08 +02:00
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
|
|
|
|
(require 'klammertext-mode)
|
|
|
|
|
|
|
|
|
|
(defgroup klammertext-indent nil
|
|
|
|
|
"Indentation for Klammertext files."
|
|
|
|
|
:group 'klammertext)
|
|
|
|
|
|
|
|
|
|
(defcustom klammertext-indent-offset 2
|
|
|
|
|
"Number of columns per klammer nesting level."
|
|
|
|
|
:type 'integer
|
|
|
|
|
:group 'klammertext-indent)
|
|
|
|
|
|
|
|
|
|
(defcustom klammertext-transparent-klammers '("document")
|
|
|
|
|
"Klammers whose span contributes no indentation level.
|
|
|
|
|
The @document klammer is transparent so that the ordinary paragraphs of a
|
|
|
|
|
document sit at the left margin; a future top-level peer (e.g. @jupyter)
|
|
|
|
|
would be registered here too."
|
|
|
|
|
:type '(repeat string)
|
|
|
|
|
:group 'klammertext-indent)
|
|
|
|
|
|
|
|
|
|
(defcustom klammertext-code-klammers '("eval")
|
|
|
|
|
"Klammers whose argument span holds code, never reindented.
|
|
|
|
|
Lines inside such a span answer TAB with `noindent'. @eval is seeded
|
|
|
|
|
because its content is Python, C++ or shell source -- Python in particular
|
|
|
|
|
is indentation-sensitive. This mirrors the Klammermachine's own rule that
|
|
|
|
|
@eval argument spans hold code, not writer text."
|
|
|
|
|
:type '(repeat string)
|
|
|
|
|
:group 'klammertext-indent)
|
|
|
|
|
|
|
|
|
|
;; --- Depth scanner ------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
(defun klammertext-indent--state-at (pos)
|
|
|
|
|
"Scan from `point-min' to POS (a line beginning).
|
|
|
|
|
Return (STACK . OPAQUE): STACK is the list of names of the klammer
|
|
|
|
|
applications, @@ definitions and @@@ commands open at POS, innermost
|
|
|
|
|
first; OPAQUE is
|
|
|
|
|
non-nil 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). Reuses the mode's classification helpers, so escaped ^@ /
|
|
|
|
|
^#, the abbreviated @name-arg form, and #+ #/ #- are all stepped over the
|
|
|
|
|
same way the font-lock scanner steps over them."
|
|
|
|
|
(save-excursion
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
|
(let ((stack nil) (opaque nil) (done nil))
|
|
|
|
|
(while (and (not done)
|
|
|
|
|
(re-search-forward "[@#]" pos t))
|
|
|
|
|
(let ((hit (1- (point))))
|
|
|
|
|
(cond
|
|
|
|
|
((klammertext--escaped-p hit)) ; ^@ / ^# : plain text
|
|
|
|
|
((eq (char-after hit) ?#)
|
|
|
|
|
(let ((next (char-after (1+ hit))))
|
|
|
|
|
(cond
|
|
|
|
|
((eq next ?#) ; ## removes to end of buffer
|
|
|
|
|
(setq opaque t done t))
|
|
|
|
|
((eq next ?\[) ; #[ ... ]# (nestable)
|
|
|
|
|
(let ((end (klammertext--block-end (+ hit 2))))
|
|
|
|
|
(if (< pos end)
|
|
|
|
|
(setq opaque t done t)
|
|
|
|
|
(goto-char end))))
|
|
|
|
|
((memq next '(?+ ?/ ?-))) ; whitespace operators
|
|
|
|
|
(t (goto-char (line-end-position)))))) ; # to end of line
|
|
|
|
|
(t ; an @-run
|
|
|
|
|
(let* ((run-end (klammertext--at-run-end hit))
|
|
|
|
|
(len (- run-end hit)))
|
|
|
|
|
(cond
|
|
|
|
|
;; @name / @@name / @@@name : an opener (or, for a literal
|
|
|
|
|
;; klammer, a verbatim span to step over).
|
|
|
|
|
((klammertext--name-char-p (char-after run-end))
|
|
|
|
|
(goto-char run-end)
|
|
|
|
|
(skip-chars-forward "A-Za-z0-9_")
|
|
|
|
|
(let ((name (buffer-substring-no-properties run-end (point))))
|
|
|
|
|
(cond
|
|
|
|
|
((and (= len 1)
|
|
|
|
|
(member name klammertext-literal-klammers))
|
|
|
|
|
;; Verbatim interior: find the closing NAME@ by name.
|
|
|
|
|
(if (re-search-forward
|
|
|
|
|
(concat (regexp-quote name) "@") nil t)
|
|
|
|
|
(when (< pos (point))
|
|
|
|
|
(setq opaque t done t))
|
|
|
|
|
(setq opaque t done t))) ; never closed
|
|
|
|
|
((and (= len 1) (eq (char-after) ?-))) ; @name-arg : no span
|
|
|
|
|
(t (push name stack)))))
|
|
|
|
|
;; a bare @-run, or the run of a named close: a close.
|
|
|
|
|
(t
|
|
|
|
|
(pop stack)
|
|
|
|
|
(goto-char run-end))))))))
|
|
|
|
|
;; Inside the argument span of a code klammer (e.g. a multi-line @eval)?
|
|
|
|
|
(unless opaque
|
|
|
|
|
(let ((s stack))
|
|
|
|
|
(while s
|
|
|
|
|
(when (member (car s) klammertext-code-klammers)
|
|
|
|
|
(setq opaque t s nil))
|
|
|
|
|
(setq s (cdr s)))))
|
|
|
|
|
(cons stack opaque))))
|
|
|
|
|
|
|
|
|
|
(defun klammertext-indent--depth (stack)
|
|
|
|
|
"Number of indentation levels STACK contributes.
|
|
|
|
|
Transparent klammers contribute none."
|
|
|
|
|
(let ((d 0))
|
|
|
|
|
(dolist (name stack d)
|
|
|
|
|
(unless (member name klammertext-transparent-klammers)
|
|
|
|
|
(setq d (1+ d))))))
|
|
|
|
|
|
|
|
|
|
;; --- Line classification ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
(defun klammertext-indent--dedent-line-p ()
|
|
|
|
|
"Non-nil when the current line 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."
|
|
|
|
|
(save-excursion
|
|
|
|
|
(back-to-indentation)
|
|
|
|
|
(let ((c (char-after)))
|
|
|
|
|
(cond
|
|
|
|
|
((null c) nil)
|
|
|
|
|
((eq c ?|) t)
|
|
|
|
|
((eq c ?@)
|
|
|
|
|
(not (klammertext--name-char-p
|
|
|
|
|
(char-after (klammertext--at-run-end (point))))))
|
|
|
|
|
((klammertext--name-char-p c)
|
|
|
|
|
;; A named close: name chars followed by an @-run (an unescaped @
|
|
|
|
|
;; can only be a delimiter).
|
|
|
|
|
(skip-chars-forward "A-Za-z0-9_")
|
|
|
|
|
(eq (char-after) ?@))
|
|
|
|
|
(t nil)))))
|
|
|
|
|
|
|
|
|
|
;; --- The indent function ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
(defun klammertext-indent--target-column ()
|
|
|
|
|
"Column for the current line, or the symbol `noindent'."
|
|
|
|
|
(let* ((state (klammertext-indent--state-at (line-beginning-position)))
|
|
|
|
|
(stack (car state)))
|
|
|
|
|
(if (cdr state)
|
|
|
|
|
'noindent
|
|
|
|
|
(* klammertext-indent-offset
|
|
|
|
|
(klammertext-indent--depth
|
|
|
|
|
(if (klammertext-indent--dedent-line-p) (cdr stack) stack))))))
|
|
|
|
|
|
|
|
|
|
(defun klammertext-indent-line ()
|
|
|
|
|
"Indent the current line per the Klammertext convention.
|
|
|
|
|
Content indents to `klammertext-indent-offset' x depth; a line beginning
|
|
|
|
|
with a bar run or a closing delimiter aligns with its owner's opening
|
|
|
|
|
column. Lines inside verbatim, code, or removed content are left alone."
|
|
|
|
|
(interactive)
|
|
|
|
|
(let ((target (klammertext-indent--target-column)))
|
|
|
|
|
(if (eq target 'noindent)
|
|
|
|
|
'noindent
|
|
|
|
|
(if (> (current-column) (current-indentation))
|
|
|
|
|
(save-excursion (indent-line-to target))
|
|
|
|
|
(indent-line-to target)))))
|
|
|
|
|
|
|
|
|
|
;; --- Wiring -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
(defun klammertext-indent-setup ()
|
|
|
|
|
"Enable Klammertext indentation in the current buffer."
|
|
|
|
|
(setq-local indent-line-function #'klammertext-indent-line)
|
|
|
|
|
;; Klammer indentation columns are small and semantic; never use tabs.
|
|
|
|
|
(setq-local indent-tabs-mode nil))
|
|
|
|
|
|
|
|
|
|
(add-hook 'klammertext-mode-hook #'klammertext-indent-setup)
|
|
|
|
|
|
|
|
|
|
;; Also enable in klammertext-mode buffers already open when this loads.
|
|
|
|
|
(dolist (buf (buffer-list))
|
|
|
|
|
(with-current-buffer buf
|
|
|
|
|
(when (derived-mode-p 'klammertext-mode)
|
|
|
|
|
(klammertext-indent-setup))))
|
|
|
|
|
|
|
|
|
|
(provide 'klammertext-indent)
|
|
|
|
|
;;; klammertext-indent.el ends here
|