Editor indentation for Emacs and Sublime Text; container guides point to editor support (from dev 5d35f256476e)
This commit is contained in:
276
doc/edit/sublime/Klammertext_indent.py
Normal file
276
doc/edit/sublime/Klammertext_indent.py
Normal 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")
|
||||
Reference in New Issue
Block a user