Files
klammertext/doc/edit/emacs/klammertext-indent.el

230 lines
9.9 KiB
EmacsLisp
Raw Normal View History

;;; 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).
;;
;; 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.
;;; 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