131 lines
4.8 KiB
Lua
131 lines
4.8 KiB
Lua
-- indent.lua --- auto-indent on newline (Arc 2).
|
|
--
|
|
-- RET (`edit.newline-and-indent`) inserts a newline plus the current
|
|
-- line's leading whitespace, verbatim, clipped at the split point
|
|
-- (Q#AI3): copying bytes is the only policy that cannot be wrong about
|
|
-- tabs-vs-spaces, and the clip keeps a split inside the indent from
|
|
-- double-indenting the carried text. The whole thing is ONE edit — one
|
|
-- undo step, one CRDT op. With a region it is one `buf:replace` (CUA
|
|
-- type-over, Q#AI4); the selection is cleared after every successful
|
|
-- edit, region or not (a zero-length selection would otherwise go live
|
|
-- the moment the cursor moves off the anchor). `buffer.newline` stays
|
|
-- bound-free as the plain-newline escape hatch (Q#AI2).
|
|
--
|
|
-- Framing: docs/auto-indent-framing.md.
|
|
|
|
pmacs.indent = pmacs.indent or {}
|
|
|
|
local ed = pmacs.editor
|
|
|
|
-- Start of the line containing `pos`: chunked backward scan for the
|
|
-- last newline strictly before it (comment.lua's scan — there is no
|
|
-- line-access API on buffers; giant lines stay safe).
|
|
local function line_start_before(buf, pos)
|
|
local p = pos
|
|
while p > 0 do
|
|
local from = math.max(0, p - 4096)
|
|
local chunk = buf:slice(from, p)
|
|
local nl = chunk:match("()\n[^\n]*$")
|
|
if nl then return from + nl end
|
|
p = from
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- The indent to carry over a split at `split` (Q#AI3):
|
|
-- bytes[line_start .. min(first_non_ws, split)]. Forward chunked scan
|
|
-- from the line start, stopping at the first non-whitespace byte —
|
|
-- never materializing more of the line than the indent itself plus
|
|
-- one chunk (Enter at the end of a giant minified line must not copy
|
|
-- the whole line just to produce an empty indent). `[ \t]` rather
|
|
-- than `%s` so a CR on a CRLF line never counts as indent.
|
|
local function indent_before(buf, split)
|
|
local start = line_start_before(buf, split)
|
|
local parts = {}
|
|
local p = start
|
|
while p < split do
|
|
local chunk_to = math.min(p + 4096, split)
|
|
local chunk = buf:slice(p, chunk_to)
|
|
local ws = chunk:match("^[ \t]*")
|
|
table.insert(parts, ws)
|
|
if #ws < #chunk then break end
|
|
p = chunk_to
|
|
end
|
|
return table.concat(parts)
|
|
end
|
|
|
|
-- Right-gravity translation of `pos` through the effective edit
|
|
-- (Q#AI5; the daemon optimistic-arm shape). `estop` is the PRE-edit
|
|
-- end of the replaced range; an insert has estart == estop.
|
|
local function translate(pos, estart, estop, einserted)
|
|
if pos < estart then return pos end
|
|
if pos > estop then return pos - (estop - estart) + einserted end
|
|
return estart + einserted
|
|
end
|
|
|
|
-- edit.newline-and-indent body.
|
|
function pmacs.indent.newline()
|
|
local buf = pmacs.window.buffer()
|
|
if not buf then
|
|
ed.set_status("no buffer")
|
|
return false
|
|
end
|
|
|
|
-- Snapshot the context BEFORE the edit (Q#AI5): intercepts run with
|
|
-- the registry borrow released and may switch window or buffer; the
|
|
-- fix-up below must never touch whatever is active afterwards.
|
|
local win0 = pmacs.window.current()
|
|
local cursor0 = ed.cursor()
|
|
|
|
local region = ed.region()
|
|
local has_region = region ~= nil and region["end"] > region.start
|
|
local rstart, rstop
|
|
if has_region then
|
|
rstart, rstop = region.start, region["end"]
|
|
else
|
|
rstart, rstop = cursor0, cursor0
|
|
end
|
|
local text = "\n" .. indent_before(buf, rstart)
|
|
|
|
-- One edit = one undo step, one CRDT op. Same intercept discipline
|
|
-- as killring/comment: a rejection reports rather than throws and
|
|
-- leaves no state behind.
|
|
local ok, estart, estop, einserted = pcall(function()
|
|
if has_region then
|
|
return buf:replace(rstart, rstop, text)
|
|
end
|
|
return buf:insert(rstart, text)
|
|
end)
|
|
if not ok then
|
|
ed.set_status("newline-and-indent rejected by buffer intercept")
|
|
return false
|
|
end
|
|
|
|
-- Context guard (Q#AI5): fix up only the window that made the edit.
|
|
if pmacs.window.current() ~= win0 or pmacs.window.buffer() ~= buf then
|
|
ed.set_status("newline-and-indent: context changed during edit")
|
|
return false
|
|
end
|
|
|
|
-- A deviating effective edit means an intercept rewrote it — the
|
|
-- interceptor's positional result stands (M6.4: kind and payload
|
|
-- are immutable). Cursor repair uses ONE formula for the clean and
|
|
-- transformed paths alike: translate the pre-edit cursor through
|
|
-- the effective edit, then goto_byte (which clamps). The clean
|
|
-- insert-at-cursor case lands at estart + einserted — right after
|
|
-- the carried indent.
|
|
local deviated = estart ~= rstart or estop ~= rstop or einserted ~= #text
|
|
if deviated then
|
|
ed.set_status("newline-and-indent altered by buffer intercept")
|
|
end
|
|
ed.goto_byte(translate(cursor0, estart, estop, einserted))
|
|
ed.clear_selection()
|
|
return not deviated
|
|
end
|
|
|
|
pmacs.command.define {
|
|
name = "edit.newline-and-indent",
|
|
description = "Insert a newline carrying the current line's indentation.",
|
|
fn = function() pmacs.indent.newline() end,
|
|
}
|