-- 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, }