33 Commits
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
7562d83198 | fix(compile): make overlay teardown atomic | |
|
|
b6e44f21d6 |
fix(compile): PR #113 round 7 — validated overlay attachment, registry-only dispose
Finding-by-finding (framing revision 13; bites via scripts/bite
against
|
|
|
|
fe04aa481b |
fix(compile): PR #113 round 6 — idempotent split-complete attachment, no-op edit guard, handle disposal
Finding-by-finding (framing revision 12; bites via scripts/bite
against
|
|
|
|
a49adc2589 |
fix(compile): PR #113 round 5 — buffer-level span translation, fragment preservation, tracked line start
Finding-by-finding (framing revision 11; bites via scripts/bite
against
|
|
|
|
b76c46603a |
fix(compile): PR #113 round 3 — UTF-8-safe renderer, observable parser reset, raw spec reads
Finding-by-finding (framing revision 9; bites via scripts/bite): 1. The CR/backspace renderer is UTF-8-safe: overwrite ranges consume WHOLE existing codepoints (range end aligned forward past continuation bytes) in ONE atomic replace of the complete text event — never a split of either side — and backspace steps to the previous codepoint boundary; out_pos stays on boundaries by induction. Pre-fix, byte-counted splits left malformed bytes on the plain rope, and under CRDT the byte-native edit rejected the mid-codepoint range, aborting the pump after events_take had consumed the batch (terminal event lost, record leaked). Bites: default acceptance (é\rX, X\ré, é\bX with exact-content, marker, clean-*errors*, baseline asserts) and a CRDT twin that pre-fix times out never reaching its exit marker. 2. parser:finish()'s reset is observable: balancing events — AlternateScreenExit for an unclosed enter, a default SetStyle for a non-default running style (now also cleared; reset() preserved it) — let consumers unwind mirrored state from the event stream alone. New Rust unit applies events to consumer state; Lua twin (r3f2) bites via the ansi.rs swap. 3. stdin/group spec fields are RAW reads: spec tables are plain data, metatable-provided fields are deliberately not honored (the compile.lua rawget posture), and a raising __index can no longer be silently absorbed as group=false, quietly disabling process-group isolation. Regression test pins both shapes: metatable-provided group=true is ignored (pgid != pid), and a hostile raising metatable spawns cleanly. Gates: fmt, clippy workspace all-targets, lib 1526, crdt lib 1700, compile acceptance 53, crdt acceptance 2, m4 101, GPU 59, workspace sweep 2505/0, git diff --check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB |
|
|
|
37fac4324a |
fix(compile): PR #113 round 2 — rule snapshots, finite indexes, shell isolation, parser reset
Finding-by-finding (framing revision 8; bites via scripts/bite): 1. Rule validation is a stable, total snapshot: validated scalar fields are copied into per-run plain tables via raw reads (rawget; metatable-provided fields deliberately not honored), so post-run mutation of the user's rule objects cannot alter an in-flight run and a hostile __index is a counted skip, not an error thrown through the pump mid-batch. The container traversal is itself pcall-protected; traversal-raise semantics are Lua-flavor-dependent (5.2+ ipairs consults __index, LuaJIT reads raw) and the test pins both flavors. 2. Capture indexes must be FINITE (floor(math.huge) == math.huge, so integrality alone passed it); math.huge is now a counted malformed entry. 3. Shell-command never touches the rule table: no spurious compile-rule warnings on M-!, and no rule-container state can block a run that performs no parsing. 4. AnsiParser::finish() (and parser:finish()) now fully resets the parser — in-flight CSI/OSC/escape state and alt-screen suppression included — so a post-finish feed parses a fresh stream. Three direct unit tests in ansi.rs plus a Lua-driven twin in the acceptance suite (the twin exists because a scripts/bite file swap replaces the in-file units along with the fix). 5. Comment corrections: fractional capture indexes read a distinct absent key (not a neighboring capture); the group-coercion comment describes truthiness, not false; the AnsiParserLua rustdoc lists finish(). Bites: r2f1 (both shapes), r2f2, r2f3 fail against pre-fix compile.lua; r2f4 fails against pre-fix ansi.rs. Gates: fmt, clippy workspace all-targets, lib 1525, crdt lib 1699, compile acceptance 50, crdt acceptance 1, m4 101, GPU 59, workspace sweep 2501/0 (one flaky-suite rerun per the standing m8 rule), git diff --check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB |
|
|
|
d67d30bb64 |
fix(compile): PR #113 round 1 — coordinates, recovery, rules, types, EOF
Finding-by-finding (framing revision 7; every fix bite-verified via
scripts/bite against the pre-fix tree):
1. Stored coordinates must be finite integers, and both cursor walks
are movement-bounded — they clamp at EOF, and the column walk
clamps at the target row's EOL instead of marching onto later
rows. An astronomical %d+ capture can no longer hang the editor.
2. The grep panel gains the same immediate buffer.after-edit
recovery trigger as the compile slots: M-x buffer.undo after a
COMPLETED search is marked synchronously.
3. The rustc arrow rule uses the framing's ([^:]+) spelling — paths
with spaces capture whole.
4. All pattern captures are collected (index 4+ reads the real
capture, not nil-as-column-0); capture indexes must be positive
integers; a rule naming a column its match didn't produce rejects
the match.
5. emit_text_raw is module-local — a user global could shadow the
helper the terminal-event path depends on, and its error consumed
the terminal event before pump cleanup/forget ran.
6. stdin/group spec fields reject wrong Lua types as hard errors;
group is matched as a raw Value because mlua's bool conversion
applies Lua truthiness ("true" would silently coerce).
7. resync also nils the public line_start_byte — total pre-marker
anchor invalidation includes the byte anchor.
8. The inherited cwd resolves through
pmacs.instance.identity().working_directory; the header always
names a real path and relative error files get an explicit base.
9. New AnsiParser::finish() + parser:finish() (additions #5): a
truncated multibyte sequence at process EOF surfaces as U+FFFD
before the exit marker instead of vanishing.
10. The built-in default rules are a private deep copy — in-place
mutations of the public table no longer survive the "using
built-in defaults" degradation.
Eleven new tests (r1f1a/b–r1f10); bites: 9 fail against pre-fix
compile.lua, r1f2 against pre-fix default.lua, r1f6 against pre-fix
lua_bindings/mod.rs — all clean assertion failures. Gates: fmt,
clippy workspace all-targets, lib 1522, crdt lib 1696, compile
acceptance 45, crdt acceptance 1, m4 101, GPU 59, workspace sweep
2493/0, git diff --check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
|
|
|
|
a2b12dc9d6 |
docs+test: gate fixes and handoff snapshot (compile-mode in flight)
cargo fmt over the new files; doc-markdown backticks; is_ok_and in
the recompile counter wait; m4_6's M-g n/p pin updated to the Q#CM5
takeover contract (error.next/error.previous with the diag commands
as the dispatchers' fallback — the test's no-attachment status
behavior is unchanged). Handoff §1: main @
|
|
|
|
a7d5a6fedf |
feat(process): group lifecycle + null stdin; buf:revision(); jump_back hook parity
Supervisor (Q#CM3, framing additions 1-2): ProcessSpec gains stdin="null" (Stdio::null, no writer thread, immediate EOF) and group=true — process_group(0) spawn, group-directed fatal signals, liveness-probed TERM-to-KILL reap ledger (insert-if-absent arming, per-tick kill(-pgid,0) probe, GROUP_TERM_GRACE=500ms), leader-exit group TERM before the final drain with in-drain deadline enforcement plus ESRCH quiescence window and absolute cancel cap, poll-based cancellable readers (nix poll feature added), shutdown ledger force-kill + probe-to-ESRCH, maybe_restart gated once shut_down. Both options are pipe-mode-only and rejected at spawn under PTY. Nine unit tests cover framing acceptance 34, including the TERM-ignoring redirected survivor, the pipe-holding descendant tick bound, and setsid-escapee resource reclamation via the per-runtime active-reader counter. Bindings (additions 3-4): buf:revision() exposes the edit revision (bumped by edit/undo/redo — unit-pinned); pmacs.editor.jump_back now fires buffer.after-switch exactly when the jump changed buffers, matching pmacs.window.switch_buffer; pmacs.process.spawn parses the stdin/group spec keys. Deviation from the framing letter, called out for review: the active-reader counter field is always present (one Arc + two atomics per reader lifetime) rather than cfg(test)-gated — gating the field would spread cfg attributes through every construction site; only the probe accessor is test-gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB |
|
|
|
ceaeb81386 |
fix(edit): PR #110 round 2 — UTF-8 well-formedness, source-buffer relevance, non-table sets
Finding 1 (medium): pair entries validate full UTF-8 well-formedness (Unicode Table 3-7), not just lead-byte length — continuation-byte shape on every trailing byte, overlong encodings (C0/C1, E0 80-9F, F0 80-8F), UTF-16 surrogates (ED A0-BF), and beyond-U+10FFFF (F5+, F4 90+) all disqualify, so "(\xC2x" can no longer inject invalid bytes as a closer. char_at shares the validator and returns the raw byte for malformed buffer content: the predicate treats junk as word-like (no pairing before it), never as EOL. Bite: malformed_utf8_pair_entries_are_rejected (four ill-formed shapes). Finding 2 (low): relevance and reporting resolve against the SOURCE buffer the record names, not whatever buffer a context-switching command left active. New pmacs.lsp.buffer_language(buf) is the parameterized primitive (active_buffer_language delegates), backed by a new buf:path() query on buffer handles. Bites: rust→python `'` now stays silent; python→rust `'` now reports "source context changed". Finding 3 (low): non-table set containers degrade language→default→empty instead of throwing from the after-edit callback on every keystroke. Bites: a string default pairs nothing with a clean *errors* buffer; a junk language entry falls back to the default set. Framing synced to revision 5 (Q#AP2 well-formedness + container degradation + source-buffer resolution, Q#AP3 predicate junk-byte posture, acceptance list). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB |
|
|
|
223e26420b |
feat(edit): auto-pairing (Arc 2)
Typing an opener inserts the closer with the cursor between; typing a closer over its twin steps over it. Q#AP1: the nine built-in pair chars leave both optimistic classifiers (shared charset in pmacs-protocol) and round-trip through dispatch, so the opener and the hook's closer are adjacent daemon-peer undo units, dispatch CUA type-over applies, and skip never paints a transient duplicate. Q#AP9: exact one-shot typed-edit provenance. EditorCore's apply_active_edit now returns the effective Edit; the dispatch fallback arms a per-frontend record (codepoint + requested vs effective ranges + post-cursor + clean verdict) that insert primitives complete and the daemon's optimistic CRDT arm builds directly. The record is takeable exactly once via pmacs.editor.take_typed_edit() during the one after-edit fan-out, then cleared — paste, programmatic edits, manual hook runs, nested re-runs, rejected edits, and stale this_command all observe nil, and transformed / relocated / context-switched source self-inserts fail closed with a status. pair.lua (loaded BEFORE lsp.lua — ordering contract in editor.rs): per-language pmacs.pair.sets with a conservative default (no ' or `), EOL/whitespace/closer insertion predicate, reactive skip-over-close, rejected/transformed intercept outcomes with context-guarded translate-and-clamp cursor repair. Acceptance: 32 dispatch-driven cases (predicate, skip, per-language sets, non-typed provenance incl. production-shaped paste, type-over, undo/redo grain, intercept outcomes on both the source and reaction edits, context-switch probe, record lifecycle, frontend isolation) + first-didChange ordering against the fake LSP's sighelp mode via a new PMACS_FAKE_LSP_CHANGE_SINK replay file. Six two-replica CRDT cases pin dispatch-route convergence with cursor-between, undo/redo walking the pair on both replicas, both mixed-history undo models as named substrate limits, and the optimistic custom-char route (closer-broadcast-before-opener convergence, degraded cross-peer undo). TestDaemon gains spawn_with_config for init.lua-extended pair sets. Framing: docs/auto-pairing-framing.md (revision 3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB |
|
|
|
5c2a27aaf9 |
fix(lsp): negotiate delta before requesting; input-origin signature trigger
Addresses the four post-merge findings against PR #102 (merged as
|
|
|
|
8da143b402 |
fix(edit): exact effective-edit verification for kill/yank-pop
Addresses the PR #103 round-3 review: length-delta verification is defeated by an intercept that rewrites an op to a DIFFERENT equal-length range, and "replacement text appears at start" is defeated by one that enlarges `end` by a byte. The buffer mutators (buf:insert/delete/replace) now RETURN the effective edit — `(start, end, inserted_len)` of the post-intercept operation actually applied (they returned nothing before, so no caller breaks). killring compares those against what it requested: - C-k / cut: any deviation (shifted range, resized range, nonzero insertion) means the bytes removed are not the bytes sliced — the ring and OS clipboard receive nothing, the chain clears, and the interceptor's result stands. cut now goes through buf:delete (for the effective edit) with explicit clear_selection + goto_byte. - M-y: any deviation from (s.start, s.stop, #entry.text) drops the session — including the end+1 enlargement that silently deleted an extra byte while passing the old text-at-start check. The redundant post-replace slice verify is gone; the exact contract replaces it. Tests (kill_ring_acceptance now 30): equal_length_shifted_delete_does_not_feed_the_ring (delete shifted +2, same length — the case a length delta cannot see), stop_enlarging_replace_ends_the_yank_session (mid-buffer yank so the enlarged range is valid and the transform path — not range validation — is what fires; at buffer end the same intercept fails validation and takes the rejection path, which also drops the session). Gates: fmt + workspace clippy clean; lib 1500; crdt 1672; killring 30; cua 5; m6_4/m6_5 repl (mutator-heavy) 15/11; git diff --check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|
|
|
04314ce132 |
feat(edit): kill ring + yank-pop on a per-frontend command-boundary substrate
Arc 2 (docs/kill-ring-framing.md, rev 3 — three review rounds). Kills
accumulate in a ring; consecutive kills append; C-y yanks the head; M-y
right after a yank cycles older entries; C-k (kill-line) exists at last.
The ring is daemon-global (Emacs-daemon model); chains and yank sessions
are per-frontend.
The substrate (Q#KR2): EditorCore.command_history maps FrontendId ->
{this, last} command. Every input path updates it --- the rev-1 design
treated dispatch_key as the only input path and review falsified that
twice:
keybound command dispatch_key Run arm rotate
typed char (round-trip) self-insert fallback rotate
unbound key dispatch_key unbound arm break
GPU optimistic edit handle_remote_crdt_op break
pointer gesture dispatch_mouse + dispatch_pointer break
inbound OS paste unified paste route break
menu item menu_invoke_active rotate
M-x accept pmacs.command.invoke_interactive rotate
invoke_interactive gives Emacs's execute-extended-command semantics
(M-x kill-line then C-k appends; C-k then M-x kill-line does not); the
public pmacs.command.invoke stamps nothing. Wheel scroll deliberately
does NOT break (mwheel-scroll vs mouse-set-point, as in Emacs).
Three shipped bugs fixed en route (Q#KR10):
- Semantic-path Paste was dropped ("no grid-less effect yet"), and the
GPU always negotiates semantic render --- GPU Ctrl-V was a no-op. Paste
is now a dispatcher-level arm serving both attachment kinds.
- That arm keys off the dispatcher's AUTHENTICATED source; the old grid
arm trusted the client-supplied payload frontend_id, letting a forged
id paste into another frontend's active window (unit-tested).
- Paste, M-x-invoked commands, and menu-invoked commands never fired
buffer.after-edit (each runs outside dispatch_key's revision check),
so LSP/syntax/autosave missed those edits. A shared
with_after_edit_check helper now wraps all three sites; scope is
honest --- active-buffer compare, sound for these paths, not a general
any-buffer guarantee (buffer-aware edit epoch deferred).
The ring (killring.lua, Q#KR4-7): entries carry stable monotonic ids.
Append requires last_command in the kill family AND this frontend's
last_kill_id == the head's id --- A-kill/B-kill/A-kill pushes fresh
instead of corrupting B's entry. Yank sessions store {buffer, start,
stop, entry_id, text}: M-y validates last_command + live session + same
buffer + slice(start,stop) == text (out-of-bounds reads as changed ---
pcall'd; an early test caught the guard throwing on an upstream
deletion instead of refusing), rotates by locating the entry_id's
CURRENT position (positions shift under other frontends' pushes; ids
don't), verifies the applied replace (intercepts may alter it; accepted
post-hoc semantics), then goto_byte. Failed kills clear last_kill_id;
failed/refused yanks create no session, so a second invalid M-y cannot
ride the first's name-stamp.
OS clipboard: ring head mirrors to the ACTING frontend's OS clipboard
only (pending_clipboard's existing shape; frontends may be different
machines). External content joins the ring at yank time via the
clipboard_get slot check (an OS copy reaches the daemon only when
pasted). New core seams: clipboard_set(bytes) / clipboard_get.
Lifecycle (Q#KR11): SessionDetached prunes command_history and fires the
new frontend.detached hook (raw id); killring.lua drops that frontend's
tables.
pmacs.killring.max([n]) validated (non-finite rejected --- math.huge
would defeat the cap; shrink trims immediately), default 60.
Deferred, named: word kills (M-d/M-BS/C-BS/C-h/C-DEL discard bytes ---
needs bytes-returning deleters), C-SPC/set-mark, clipboard watching,
ring browser/persistence, C-u C-y / C-M-w, buffer-aware edit epoch,
Lua-visible intercept probe.
Tests: tests/kill_ring_acceptance.rs (24) --- chain mechanics incl. all
break rows, the M-x three-direction matrix, per-frontend interleaving
(A-kill/B-kill/A-kill; stable-id rotation under B's pushes; eviction
mid-session; upstream-edit invalidation), menu Cut via real right-click
+ menu pointer (feeds ring, fires after-edit once, chains with C-k),
external-paste integration, cap validation + shrink-trim, detach
cleanup. Plus daemon unit tests: forged-id paste lands in the
authenticated source's window and leaves the claimed frontend's chain
untouched; optimistic CRDT op breaks only the source's chain.
Gates: fmt + workspace clippy clean; lib 1500; crdt 1672; killring 24;
cua 5; query-replace 16; completion 9; autosave 29; desktop 11;
persistence 5; clobber 6; m4 90; m8 10+15; m10/m11 crdt; GPU 58;
git diff --check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
|
|
b6a7cac9ea |
fix(save): refuse to silently clobber a file changed on disk
EditorCore::save() wrote unconditionally via save_atomic and only THEN
recorded the new FileMeta. It never compared the on-disk identity against
the one the buffer read. So another editor's writes, or a `git checkout`,
were destroyed without a word --- the single worst data-loss path in the
editor, on its most-used command.
The comparison seam already existed and no caller used it: FileMeta is
PartialEq (mtime + size, sized so same-second edits still differ) and
file_io::current_meta reads it. The file_io module docstring even said
callers "should" compare before saving. Nobody did.
save() now refuses when writing would destroy content the buffer has
never seen:
* the buffer recorded a meta and the on-disk meta differs --- someone
else wrote the file;
* the buffer recorded NO meta (a `[new file]`, or a path set without
reading) yet a file now exists --- it was created underneath us.
A *missing* file is not a clobber: there is nothing to destroy, so
recreating a deleted file saves normally. An unstattable path falls
through and save_atomic reports the real error.
On refusal the status line says what happened and how to override, the
buffer keeps its unsaved edits, and `buffer.after-save` does not fire.
`M-x buffer.save-anyway` (ed.save_ignoring_disk_changes) overwrites
deliberately and re-syncs the meta, so an ordinary save works again.
Named `buffer.save-anyway`, not `save-buffer-anyway`, for two reasons: it
belongs in the `buffer.` namespace next to `buffer.save`, and the latter
outranked `buffer.save` as an M-x completion for "save" (which a lib test
caught).
This is the bug the autosave arc kept circling: Q#AS5's Fresh/Stale guard
refuses to auto-offer a recovery for an externally-changed file, but
nothing stopped save() from overwriting that same file.
Tests: tests/save_clobber_guard_acceptance.rs (6) --- refuses and leaves
their content intact, after-save does not fire on refusal, save-anyway
overwrites and re-syncs meta, unchanged files save repeatedly (the guard
must not trip on our own writes), a deleted file is recreated not
refused, and a `[new file]` buffer refuses once someone else creates the
file. Verified the tests bite: 4 of 6 fail with the guard disabled.
Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; clobber 6; m1/m3
/m4 90/m5.8/m7.8/m8 10+15; autosave 29; desktop 11; persistence 5;
query-replace/completion/listview/overlay/cua green; GPU 58;
git diff --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|
|
b8a296f0a3 |
fix(persistence): only recover/discard may release unclaimed crash data
Addresses the PR #100 review round 2. Q#AS12's ownership rule guarded the sweep but not the RELEASE paths, so three doors were still open. The rule is now total: exactly two things may release an unclaimed recovery file --- recover-file (which adopts it) and discard-recovery (explicit user intent). Not a sweep, not a save, not a kill. - HIGH: buffer.after-save called _discard_buffer unconditionally, which removed the live buffer's current-path key without checking ownership. Repro: session 1 autosaves and crashes; session 2 opens the file, does not recover, then saves --- the crash artifact was deleted. Same door was open on kill. discard_buffer now removes ONLY keys this session owns. The unclaimed copy survives (reported Stale, so never auto-offered, but still recoverable/discardable). The on-disk file holds the new work; the crash copy holds work never written anywhere, so deleting it was the same data loss by a different door. - MEDIUM/LOW: _adopt only recorded the path in `owned`, not an association with the buffer. A removal callback fires after the buffer has left the registry, so discard_buffer had no path to read and no `written` entry to fall back on --- recover-then-kill leaked the copy and it was offered again. adopt now takes the BUFFER and records a `written` entry at the revision whose contents the file holds. That is correct twice over: the skip cache declines to rewrite an identical copy, and a kill can find and retire it. - LOW: _discard(path) removed the file and unowned the hash but left matching `written` entries, so a still-dirty buffer hit the unchanged (path_hash, revision) fast path and went unprotected until its next edit. discard_path now clears those entries; the next sweep re-protects immediately. Tests (autosave_acceptance now 24): saving_without_recovering_preserves_unclaimed_crash_data, killing_without_recovering_preserves_unclaimed_crash_data, recover_then_kill_retires_the_adopted_recovery, discard_recovery_lets_the_next_sweep_reprotect_immediately. Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 24; desktop 11; persistence 5; GPU 58; git diff --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
1205329c34 |
fix(persistence): never clobber unclaimed crash data; buffer-keyed cleanup
Addresses the PR #100 review. - HIGH data loss: sweep could overwrite an existing crash recovery before the user ran recover-file. Reopen a file after a crash, edit it, and the next autosave wrote the current buffer over the recovery key --- losing exactly what autosave exists to protect. New ownership rule (Q#AS12): a per-session `owned` set records which path hashes THIS session wrote or adopted. A recovery file at a key we do not own is unclaimed crash data; the sweep refuses to write that buffer, counts it `blocked`, and says so ("autosave paused for N file(s) with unclaimed recovery"). recover-file ADOPTS the copy once its contents are in the buffer; discard-recovery removes it. Either resumes normal autosave. sweep() now returns (written, blocked). - MEDIUM cleanup missed paths autosave can write. Kill/save cleanup now goes through `discard_buffer(BufferId)`, which removes BOTH the buffer's current-path key and the key its last sweep actually wrote (they differ after a rename --- an LSP WorkspaceEdit changes the path while the BufferId stays; a path-captured callback deleted the wrong key). And a sweep-time GC deletes the recovery of any buffer that left the registry, which is the backstop for argv `[new file]` buffers: they fire no after-load, so no removal callback is ever registered for them. - LOW/MEDIUM recover-file pinned only on the active path. Two buffers can visit one path (pmacs.buffer.from_file does not dedup), so focus drift could recover into the wrong buffer. It now captures and compares the origin buffer handle as well as the path. - LOW write_private left a pre-existing lax autosave/ directory alone. The birth-mode only applies to dirs that call creates, so a 0755 autosave/ from an older run still leaked recovery-file names, sizes, and mtimes despite 0600 contents. It is now tightened to 0700 --- but never `base` itself, which is shared with history/recentf/desktop and may predate us. New `state::exists` (an existence check, no read) backs the ownership gate. Tests (autosave_acceptance now 20): sweep_never_overwrites_unclaimed_ crash_recovery (blocked, crash copy byte-identical, adopt resumes), discarding_an_unclaimed_recovery_unblocks_the_sweep, killing_a_new_file_buffer_gcs_its_recovery, saving_after_a_rename_removes_the_recovery_written_under_the_old_path, a_pre_existing_lax_autosave_dir_is_tightened. Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 20; desktop 11; persistence 5; m4 90; m7_8 5; GPU 58; git diff --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
ec42526652 |
feat(persistence): autosave + crash recovery (Arc 3 phase 3)
Framing: docs/autosave-recovery-framing.md (Q#AS1-11). Closes the
persistence arc. Every modified file buffer is periodically written to a
private recovery copy; if pmacs dies, the next session says so and
`M-x recover-file` installs it. Emacs's auto-save-mode + recover-file.
Hybrid, forced by the same two gaps as phase 2: Lua has no per-buffer
path getter and FileMeta is neither Lua-visible nor serde. Rust owns the
sweep and the external-change guard; Lua owns cadence, config, and UX.
src/autosave.rs (new):
- One atomic envelope per recovery: a JSON header line + `\n` + raw
buffer bytes. Split at the FIRST newline, so contents may hold newlines
and non-UTF-8. A crash can never leave a torn header/contents pair.
- `origin` is NULLABLE: a `[new file]` buffer (a path with nothing on
disk) has no FileMeta, and its unsaved contents are exactly the work
most worth recovering.
- status(): Fresh / Stale / Corrupt / None. Only Fresh is announced;
Stale (file changed, deleted, or created underneath us) is never
auto-offered; Corrupt is typed, quiet, and discardable.
- sweep(): all modified file buffers, skipping clean/scratch and those
unchanged since their last copy. The skip cache is keyed
BufferId -> (path_hash, revision), not revision alone: a buffer keeps
its BufferId across a path change (LSP WorkspaceEdit rename), so a
revision-only cache would skip the write and orphan the old key.
- pending(): enumerates ALL open file buffers in Rust, which is what
covers argv `[new file]` buffers -- they fire no hook at all.
Private storage (Q#AS11, a precondition for default-on): autosave stores
unsaved FILE CONTENTS, not metadata. New `file_io::save_atomic_with_mode`
sets the temp's mode BEFORE the rename (a chmod-after-write leaves a
window where the file is 0644), and `state::write_private` creates the
dir 0700 and the file 0600. Plus `state::read_bytes` (state::read is
read_to_string, which non-UTF-8 buffer contents would fail).
builtin/runtime/autosave.lua:
- Cadence is `process.after-tick` + monotonic_ms, NOT workers.sleep: a
long sleep parks one of only `available_parallelism - 1` pool threads,
and re-reading the interval each tick makes it live-reconfigurable.
- pmacs.autosave.interval_ms([ms]) -- validated getter/setter following
the async_config.frame_target_ms shape. Default 30000, floor 1000.
pmacs.autosave.enable(on). On by default.
- Notify, never prompt: `after-load` only raises a flag; the tick emits
ONE aggregate message ("3 files have autosave recovery"). A modal
prompt from after-load would stack N modals during a desktop restore.
- recover-file confirms, pins to the origin buffer, replaces contents,
then explicitly fires `buffer.after-edit` -- the mutators only notify
windows and queue CRDT, and after-edit comes from dispatch_key's
post-command check, which the minibuffer shadow returns before. Without
the explicit fire, LSP didChange and the syntax reparse never see the
recovery. discard-recovery deletes a copy (including a Corrupt one).
- Cleanup: after-save discards; per-buffer on_removed discards on kill
(there is no global kill hook); before-quit does one final synchronous
sweep and never vetoes.
src/hash.rs (new): one pub(crate) sha256_hex, shared by desktop, autosave,
and packages::fetcher -- which had two private duplicates (Q#AS9).
Not daemon-gated (unlike desktop-save): autosave is per-buffer, not
per-frontend, and a daemon holds the unsaved work.
Tests: 8 autosave units + 13 state/hash units + tests/autosave_acceptance
(15): sweep round-trip, non-UTF-8 envelope, [new file] null-origin
Fresh->Stale, 0600/0700 perms, skip clean/scratch/unchanged, path-change
rewrites new key + discards old, save/kill cleanup, Stale not offered,
Corrupt typed+quiet+discardable, recover-file installs + fires after-edit
+ leaves modified, tick aggregation (3 loads -> 1 message, no repeat),
single-file naming, interval validation + live change, enable gate,
before-quit sweeps without vetoing.
Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 15;
desktop 11; persistence 5; m4 90; m7_8 5; m8 10; GPU 58; git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|
|
e56c3055f2 |
fix(persistence): reliable daemon gate, unarm, per-pane after-load
Addresses the PR #99 review: - HIGH daemon local-only was not reliable: run_daemon sets DaemonMode only after EditorState::new() has run init.lua, so desktop_mode(true) in init saw is_daemon()==false and the raw bindings were ungated. Now save_session/restore_session early-return in Rust when the DaemonMode marker is present — set right after the daemon's new(), so it holds for every save/restore that can run after startup (before-quit hook, manual commands, direct binding calls). - MEDIUM desktop_mode(false) could not unarm startup restore: arm_restore is now a boolean (arm_restore(on)) that sets/removes the marker, and desktop_mode(on) calls arm_restore(on). enable-then-disable no longer restores. - MEDIUM/LOW same-file multi-pane missed per-window overlays: restore now fires buffer.after-load once PER LEAF (per window), not once per buffer. Syntax attaches its overlay to the active window, so each pane gets its own; LSP attach_buffer is idempotent, so the same file in two panes attaches LSP once but syntax to both. - MEDIUM hidden restored buffers: documented as registry-only in v1 (they are live/openable/in recentf, but do not fire after-load, so they attach syntax on first visit via after-switch and LSP when next shown). Full initial attach for hidden buffers is deferred. Noted in the framing + a code comment. - LOW trailing whitespace in docs/desktop-save-framing.md. Tests (desktop_acceptance now 11): same_file_..._fires_per_pane asserts after-load fires twice for two panes of one file; daemon_mode_disables_ save_and_restore; disabling_desktop_mode_unarms_restore. Gates: fmt + workspace clippy clean; lib 1487; crdt 1658; desktop 11; persistence 5; m4 90; GPU 58; git diff --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
3607df0afe |
feat(persistence): desktop-save --- buffers + layout + positions (Arc 3 phase 2)
Framing: docs/desktop-save-framing.md (Q#DS1-10). Save the open file
buffers, window layout, and per-window positions on quit; rebuild them
on startup. Emacs desktop.el, opt-in, local-mode only in v1.
All-Rust (Q#DS1) --- the core window enums are not serde and there is
no Lua tree API, so the layout mirror + structural rebuild live in Rust.
Lua adds only the opt-in switch and manual commands.
src/desktop.rs (new):
- Serde mirror (SavedDesktop / SavedBuffer / SavedNode / SavedLeaf /
SavedOrientation): every open file buffer (visible OR hidden, so a
switched-away file survives), the layout tree with orientation +
weights + nesting, per-leaf cursor/view_top, and an active-leaf
preorder index with a nearest-neighbor fallback (Q#DS10).
- session_key: SHA-256, name.<hex> when a socket name is set else
cwd.<hex> (charset-safe for the pmacs.state key).
- save_session / restore_session take the &Lua that carries the
SharedCore / StateDir / LocalInstanceInfo app-data, so they run
identically from a pmacs.session.* binding and the startup trigger.
- restore ordering (Q#DS3): open all buffers; prune EVERY window of the
old LOCAL layout (not just scratch); rebuild the tree; then per leaf
in preorder activate its window and fire buffer.after-load once per
newly-loaded buffer (hooks read active state), and write the exact
cursor/view_top AFTER so desktop wins over saveplace (same file in two
panes keeps distinct positions). A missing file collapses its leaf.
src/editor_core.rs: get_or_load_buffer(path) --- find_by_path else
load fresh, WITHOUT switching the active window; returns (id, newly).
src/lua_bindings: pmacs.session.{save_desktop, restore_desktop,
arm_restore, is_daemon}; DesktopRestoreArmed + DaemonMode markers;
fire_after_load_hook seam.
builtin/runtime/desktop.lua: pmacs.session.desktop_mode(on) wires
before-quit save + arms restore; desktop-save / desktop-restore
commands. No-op under a daemon (Q#DS9).
Startup trigger (Q#DS7): editor::run captures had_file before the match
consumes `file`, and restore_desktop_if_armed runs INSIDE the RunLocal
arm (after attach dispatch) so a hand-off to attach never populates an
EditorState it is about to drop. Daemon marks DaemonMode → desktop
stays local-only.
Tests: src/desktop.rs units (tree collapse, active-leaf fallback,
key/json round-trip) + tests/desktop_acceptance.rs (9): nested weighted
round-trip, hidden-buffer survival, after-load-active probe, same-file
two-pane distinct positions, missing-file collapse + focus fallback, no
orphan windows, name-vs-cwd key scoping, modified warning, startup gate.
Gates: fmt + workspace clippy clean; lib 1487; crdt 1658; desktop 9;
persistence 5; m4 90; m8 daemon 10/15; query-replace/completion/
listview/overlay/cua green; GPU 58; git diff --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|
|
d5d75e63ed |
fix(persistence): symlink confinement, real test-inertness, view_top restore
Addresses the PR #98 review: - HIGH symlink escape: resolve() did only a lexical starts_with, so a base/autosave symlink -> /tmp/out let state.write("autosave/x") write outside the state dir. Now every existing component the key adds under base is lstat'd and a symlink (live OR broken) is rejected; base itself may still be a symlink (dotfile-managed ~/.local/state). Unix symlink escape test added (live + broken + plain-subdir-ok). - MEDIUM integration-test state leak: the state/history dir wiring moved out of EditorState::new() into EditorState::install_state_dirs(), called only by the real entry points (editor::run, run_daemon). Unit AND integration tests construct EditorState directly, so they never configure a real dir -> default-on recentf/saveplace write nothing to ~/.local/state/pmacs during cargo test. The inertness test now asserts a bare new() leaves StateDir unconfigured (direct proof). - MEDIUM saveplace never recorded view_top: exposed the missing pmacs.editor.view_top() getter (set_view_top existed but no getter, so the Lua stored 0). saveplace now records+restores the viewport; acceptance asserts view_top restores, not just the cursor byte. - MEDIUM/LOW relative XDG_STATE_HOME / PMACS_STATE_HOME: a relative value rooted state at a cwd-relative pmacs/... (same footgun class as the empty case). Both are now required absolute; relative values are ignored (XDG falls through to HOME). Test added. - LOW trailing blank line at recentf.lua EOF (git diff --check). Gates: fmt + workspace clippy clean; lib 1483; crdt 1654; persistence 5; m4 90; m8_1/m8_2 daemon 10/15; query-replace/completion/listview/overlay/ cua green; GPU 58; git diff --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
4dd4b9ab97 |
feat(persistence): state foundation + saveplace + recentf (Arc 3 phase 1)
Framing: docs/persistence-framing.md. The four Rust primitives the
Lua-vs-Rust scout said were unavoidable, plus two Lua policy modules.
Rust:
- src/state.rs: state_dir(xdg,home) returning .../pmacs (generalizes
the baked-in history path). Deliberate empty-XDG fix (Q#PS2): a blank
XDG_STATE_HOME fell through to a RELATIVE pmacs/... path (a cwd-write
bug); now treated as absent so it falls to HOME. Confined key->file
store: validate_name rejects absolute / .. / empty / // / control
chars, plus a canonical-prefix belt; read/write/remove go through
file_io::save_atomic, never raw io.open. A PMACS_STATE_HOME override
lets CI / privacy-conscious users / integration harnesses redirect
all state to a scratch dir. History routed through the shared
resolver so it honors the override too.
- pmacs.state.{write,read,remove,path,available}: a no-op when the
state dir is unconfigured (cfg(test) / no HOME), so default-on
builtins write nothing in the lib suite. Configured once at startup
like history_dir, skipped under cfg(test).
- pmacs.editor.goto_byte / set_view_top: byte-exact restore (switch
zeroes the cursor).
Lua (builtin/runtime):
- saveplace.lua: record the active file's cursor+view_top on
before-save / before-quit; restore on after-load. LRU-capped places
state file. On by default; pmacs.saveplace.enable(false).
- recentf.lua: MRU record on after-load AND after-switch (re-visits
refresh the order); deduped/capped recentf file; a recent-files
command bound C-x C-r opens the minibuffer picker.
Tests: state.rs units (validate/resolve/round-trip/empty-XDG),
tests/persistence_acceptance.rs (state round-trip + confinement
rejections, inert-when-unconfigured, recentf MRU/dedup, saveplace
restore-on-reload, disable knob) injecting a tempdir state root. One
describe-hook test made robust to a builtin now subscribing to
buffer.before-save.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|
|
7e7b3f2dcd |
feat(edit): query-replace (M-% / C-M-%) — Arc 2
Emacs query-replace, built on isearch with zero protocol change (framing: docs/query-replace-framing.md). - search.rs: find_first_from (literal) + find_first_regex_from (cached engine, zero-width-skip) + compile_search_regex (shared smart-case compile). Q#QR2's forward-scan-past-replacement primitive. - QueryReplaceSession + core methods (editor_core.rs): begin (invalid regex refuses, Q#QR2), replace/skip/all/replace-and-quit/finish; matches run forward from next_from on the LIVE buffer, so offset shifts and never-re-matching-replacements (a->aa) fall out for free; current match highlighted via a single-element search_store set (SearchMatchActive, both frontends free) + cursor reveal; quit keeps replacements, only nothing-matched restores origin (Q#QR10). - Dispatcher shadow (editor.rs): QueryReplaceKey (y/SPC, n/DEL, !, ., q/RET/Esc/C-g) + dispatch_query_replace_key, the 5th modal shadow; added to dispatch_idle disjunction (GPU round-trips keys) and fires buffer.after-edit itself (Q#QR1 — a shadow returns before the normal post-command check; once per !-batch). - Lua: ed.query_replace_start/query_replace_active; query-replace / query-replace-regexp commands (chained minibuffer.read, separate from/to history buckets, empty-from reject / empty-to deletion); M-% / C-M-% bindings. - Per-match prompt via core.status → v15 StatusFacts.message band. Tests: 7 core unit + 11 dispatch_key acceptance (replace/skip/!/./quit, nothing-matched restore, empty-to deletion, a->aa non-loop, regex incl invalid, after-edit fires, dispatch_idle gate, explicit M-% AND C-M-% binding tests) + 5 search unit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
5c5caa9482 |
fix(panels): follow active buffer on semantic frontends; re-attach overlays on switch
Two PR #94 validation findings. 1. (High, blocking) GPU stuck after leaving a panel: the GPU only swaps its displayed buffer on BufferSnapshot, and the daemon only sent one on the first CRDT upgrade (F29's ensure returns None for an already-backed buffer). A panel's q / RET switched the daemon's active buffer back to the already-known source and sent nothing -- the GPU kept rendering the panel while input targeted the source: a typing-into-a-buffer-you-can't-see hazard. Fix: the per-tick loop now FOLLOWS each replica frontend's own active buffer -- when it differs from the last snapshot sent to that frontend, ship that buffer's snapshot to that frontend only (the F29 broadcast records itself so the upgrade tick doesn't double-send). First-tick send also repairs the attach-time last-snapshot-wins ambiguity. Snapshot export extracted and shared with the F29 broadcast; per-fid state cleaned on both detach paths. 2. (High, wider than reported) 'LSP doesn't activate on navigate': switch_active_buffer clears the window's overlays, and the runtime dedup tables (highlighted_buffers, styled_buffers, diag_viewed_buffers) blocked re-attachment -- so EVERY buffer switch (plain C-x b included, long-latent) permanently stripped syntax color, LSP semantic style, and diagnostic underlines; verified: overlay kinds [syntax-highlight, lsp-style, diagnostic] -> [] after one away-and-back. Fix: a new additive buffer.after-switch hook, fired by the window.switch_buffer binding and find_or_open's existing-buffer branch; syntax.lua and lsp.lua subscribe and re-push their views (the just-cleared window makes that exactly-once per switch; fresh loads keep firing after-load). Regression: tests/overlay_reattach_acceptance.rs (double round-trip counts exactly one highlight overlay; panel q restores styling). The daemon follow path is validated live (daemon + GPU) -- its unit seam is the shared export helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|
|
|
b25b47334d |
feat(panels): listview module, Q#P6 round-trip seam, references panel
Arc 1b phase 1 (framing: docs/lsp-panels-framing.md).
Q#P6 (the one Rust change): EditorCore.round_trip_buffers +
pmacs.buffer.set_round_trip_input(buf, on); dispatch_idle() reports
false while a marked buffer is active, so semantic frontends'
optimistic-apply stays off -- RET reaches a panel's buffer-local visit
binding instead of locally inserting a newline, and typing dispatches
into the edit path where the read-only intercept rejects it (a CRDT
import would bypass the intercept chain entirely). Pruned on kill.
Q#P1/P2/P3: builtin/runtime/listview.lua generalizes the *buffer-list*
idiom -- pmacs.listview.open{name, header, rows, on_visit, on_refresh}
owns ensure-buffer (recreates if user-killed), wholesale render with
bypass_intercept, line->item map, buffer-local RET/SPC/n/p/g/q keymap,
previous-buffer capture + q restore (never another panel; scratch
fallback), cursor re-seat after render, the read-only intercept, and
the Q#P6 mark. Panels are buffers: both frontends render them with
zero protocol change.
Q#P4: lsp.find-references (M-?) opens *references* -- one row per
location, paths shortened against the project root, RET visits via the
shared SP-4 template (jump ring, find_or_open, cursor walk; extracted
as visit_location for the phase-2 outline to reuse).
Acceptance: tests/listview_acceptance.rs -- open/seat/visit, header
non-visitable, q restore, read-only rejection, dispatch_idle gate,
refresh re-render + re-seat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
|
|
c01cfd1e93 |
test(completion): popup acceptance suite; fix pmacs.completion table clobber
Seven end-to-end tests through dispatch_key: dabbrev auto-open + TAB accept, C-n/RET second-candidate accept, Esc dismiss with fall-through typing (and no same-edit reopen), Home-breaks-anchor validation close, yank-shaped edits never auto-open (Q#C9), C-M-i below the threshold, and ctx.uri scoping through the Lua provider surface. The suite immediately caught a real wiring bug: install_completion (M4.7, runs at make_lsp_manager time) built pmacs.completion with a fresh lua.create_table(), clobbering the popup bindings installed at editor-attach time --- popup_visible was nil at runtime and the driver hook errored silently into *errors* on every edit. It now merges into the existing table, the same idiom as install_completion_framework, so installer order no longer matters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|
|
|
53d771a935 |
feat(completion): Lua driver, popup bindings, LSP scoping + flush seams
Q#C1/C9: builtin/runtime/completion.lua reconstructs typing intent
from state (buffer.after-edit has no payload): a {buffer, cursor}
snapshot recognizes the single-byte-advance typing signature, so
paste/undo/kill/remote edits never auto-open; prefix >= 2 opens off
the synchronous providers, server trigger chars open a pending session
that materializes when the LSP answer lands; refresh-on-typing
re-derives the prefix from the text; a core-closed popup suppresses
reopen off the same edit (the accept case). completion.at-point on
C-M-i covers deliberate invocation; the driver filters collect() to
score >= 0 (collect keeps non-matches, merely sorted last).
Q#C8: CompletionContext gains uri; the built-in LSP provider scopes to
it (legacy global drain only when absent); Lua providers get uri as a
trailing ninth positional arg; context_for can now express char
triggers + uri. pmacs.lsp.attachment_for_request() exposes the
flushing accessor (attached_for_active) so completion requests answer
against current text, not the debounced didChange backlog.
Q#C2 write path: pmacs.completion.popup_show/popup_hide/popup_visible
publish into the core session (kind tags shared with collect() rows,
so driver code passes rows straight through).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
|
|
9ae381f740 |
feat(tui): relative + hybrid line-number modes (sub-arc 3, TUI half)
Extend the gutter with the last two of the framed modes (Q#UX4): - Relative: each line shows its distance from the cursor line (cursor = 0). - Hybrid: cursor line shows its absolute number, others relative (Vim number + relativenumber). `LineNumberMode` gains `Relative`/`Hybrid` + `number_for(line, cursor_line)` (the per-line displayed value) and `is_on()`. `paint_line_number_gutter` now derives each number from the mode and the cursor's buffer line (`text_view.line_at_offset(cursor)`); the TUI re-renders the whole frame on cursor motion, so relative numbers track the cursor for free. Gutter width is sized by `digits(line_count)` for every on-mode, so the text never jitters as the cursor moves. Mode selection (chosen over a 4-way cycle): `window.toggle-line-numbers` stays a binary off/absolute toggle; a new `window.set-line-numbers` opens the minibuffer with an arrow-navigable completion dropdown (off|absolute|relative|hybrid) to pick a mode directly. `set_line_numbers` accepts all four; the getter returns them. No protocol change here — the GPU half (which needs the mode over the wire, protocol v14) follows. Test: number_for across all modes. fmt + clippy clean both flavors; 1446 lib tests pass. Needs a TUI eyeball. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U |
|
|
|
fae7ed3fd0 |
feat(tui): line-number gutter (UX arc sub-arc 1, TUI half)
Introduce a reserved left gutter column with absolute line numbers in the
TUI/grid frontend — the foundational piece of the UX arc (docs/ux-arc-
framing.md). Default OFF (Emacs tradition), so zero layout/coordinate
change until a window opts in.
- window.rs: LineNumberMode { Off, Absolute } + per-window `line_numbers`
field + `gutter_width()` (digits(line_count) + PAD) + `decimal_digits`.
- editor.rs: the gutter is one viewport shift at the paint site
(cell_origin.col += gutter_w, cell_size.cols -= gutter_w) — every
viewport-relative painter (text, syntax, diag underline, search) stays
gutter-agnostic. The sites that read rect.origin.col directly get a
manual +gutter_w: cursor placement, local selection, mouse hit-test
(a gutter click maps to line start, Q#UX6). paint_line_number_gutter
writes right-aligned dim digits alloc-free.
- overlay_paint.rs: remote-presence cursor/selection shift by gutter_w.
- Lua: pmacs.window.set_line_numbers/line_numbers +
window.toggle-line-numbers command.
No protocol/daemon change (frontend-local, Q#UX1). Tests: gutter render
(right-aligned digits + past-EOF blanks) + decimal_digits.
Validated: fmt clean; clippy --lib clean both flavors; 1439 lib tests pass
both flavors. Needs a human eyeball (coordinate-math change) before the PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
|
|
|
|
296cf34ae9 |
refactor(lua): re-export install_* wiring fns to preserve the public API
Review follow-up on the F-016 split. install_diag / install_project_index / install_mcp were `pub fn` reachable at crate::lua_bindings::install_* be- fore the split, but moving them into private child modules dropped those paths without a re-export — shrinking the public API, which the split is supposed to preserve. (They take crate-internal handle types so no external caller can invoke them, and none does, so nothing actually broke — but the paths should still resolve.) Re-export all three alongside the factories/handles already re-exported, restoring the paths for the two already-merged tranches (diag, index) too. Deliberately narrowing these to pub(crate) is left as a separate change. Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib suite 1437 passed / 0 failed under luajit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U |
|
|
|
e8e56bdbf1 |
refactor(lua): extract pmacs.mcp into its own module (F-016, tranche 2)
Third tranche of the F-016 split. Extract the pmacs.mcp surface (MCP client bindings) from src/lua_bindings/mod.rs into src/lua_bindings/mcp.rs, verbatim. Corrected model (see framing): a helper-hoist is NOT a prerequisite for most domains. The contamination that stopped parse/theme bites only when a shared helper is *defined inside* the range being extracted. A domain that merely *uses* a cross-section helper reaches it via `super::` (parent-private access). So mcp extracts cleanly: all its items are self-contained, and it reaches the JSON converters (still in the lsp section) via super::json_to_lua / lua_to_json, and SharedProcessSupervisor via super::. The JSON-helper hoist is deferred to the tranche that extracts lsp itself (where they're defined). mod.rs declares `mod mcp;` and re-exports make_mcp_manager (external caller editor.rs) and McpServerIdLua — the latter to preserve its public-API path crate::lua_bindings::McpServerIdLua (moving it into a private module had dropped it from the crate surface; the split must not shrink the public API). Pure code motion, no behavior change. mod.rs: 14603 → 14020 lines. Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib suite 1437 passed / 0 failed under both luajit and lua54. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U |
|
|
|
df35e03ecf |
refactor(lua): extract pmacs.index into its own module (F-016, tranche 1)
Second tranche of the F-016 split. Extract the pmacs.index surface (the project symbol-index bindings) from src/lua_bindings/mod.rs into src/lua_bindings/index.rs, moved verbatim. index is the one genuinely clean remaining leaf: its private helpers (symbol_kind_from_lua, lua_symbol_from_table, search_hit_to_lua) are used only within its own range, and it has zero shared-core coupling — it depends only on crate::project_index, mlua, and std, reaching one stranded helper (lua_to_json, still in the lsp section) via `super::`. mod.rs declares `mod index;` and re-exports `SharedProjectIndexer` + `make_project_indexer` via `pub use`, so the crate::lua_bindings::… paths in editor.rs and completion_framework.rs (and an in-file completion- framework use) stay valid — no external file changes. Pure code motion, no behavior change. mod.rs: 14986 → 14603 lines. While vetting the next leaves I found the recon under-counted the misplaced shared helpers: parse/theme, window, and minibuffer trail off into shared style/color, caller_source, and command/menu helpers, so a dedicated helper-hoist tranche must precede them (framing tranche plan updated). This tranche stops at index rather than force a contaminated extraction. Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib suite 1437 passed / 0 failed under both luajit and lua54. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U |
|
|
|
d002b7da71 |
refactor(lua): split lua_bindings.rs into a module dir; extract pmacs.diag (F-016, tranche 0)
First tranche of the F-016 split of the 15k-line src/lua_bindings.rs. Deliberately minimal — it validates the mechanics before bulk moves. - Convert src/lua_bindings.rs → src/lua_bindings/mod.rs (the `pub mod lua_bindings;` in lib.rs resolves to mod.rs unchanged). - Extract the pmacs.diag surface (diagnostic_to_lua + install_diag) into src/lua_bindings/diag.rs, moved verbatim. mod.rs declares `mod diag;` and its one internal call site is now `diag::install_diag(...)`. Pure code motion: no logic, signature, or behavior change. diag.rs reaches shared-core items (BufferIdLua, SharedCore) via `super::` — a child module can see its ancestors' private items, so no visibility widening was needed; install_diag's only caller is mod.rs itself, so no re-export either. The Lua-visible pmacs.diag.* API is byte-for-byte unchanged. mod.rs: 15202 → 14986 lines. Framing + tranche plan: docs/lua-bindings-split-framing.md. Validated: fmt clean; clippy --lib clean under both Lua flavors; full lib suite 1437 passed / 0 failed under luajit AND lua54 (the tests drive pmacs.diag.* through the Lua VM — same outcomes, code relocated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U |
Renamed from src/lua_bindings.rs (Browse further)