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
Package-manager hardening sweep from the repo audit — one Medium + four
Lows, all in src/packages/ (F-011 also renames across lua_bindings + tests).
F-005 (Medium) — install dirs are named by package basename and require
routes by basename, so two distinct packages `owner/magit` and
`other/magit` collapse to one dir with most-recent-install silently
winning. Reject a resolve plan that contains distinct names sharing a
basename: new ResolveError::BasenameCollision + find_basename_collision()
in into_plan (the one place holding every name at once). The loader's
*intended* cross-scope override (project- vs user-scope, most-recent-first)
is untouched — its test still passes. Namespace-preserving layout and
cross-resolve install-time detection are named-deferred.
F-009 (Low) — the fetch bare-mirror cache dir was keyed by 64-bit FNV-1a
of the (attacker-adjacent) repo URL — trivially collidable. Swap to
SHA-256 (sha2, already a dep for lockfile hashing). normalize_url still
folds equivalent URLs to one entry; only the digest changes (re-clones
once, it's a cache).
F-010 (Low) — on a git subprocess timeout, run_with_timeout returned
before joining the stdout/stderr drain threads (joined only on the normal
path), leaving detached readers. Restructure to break the wait loop with a
Result, reap the child on every path, and join both threads at one point
before propagating.
F-011 (Low) — ResolvedPackage.commit was documented "Full 40-character
commit hash" but commit_for_tag() puts a tag string there (the resolver
works against commit-ishes by design, deferring SHA resolution to the
installer/lockfile). Rename the field to `revision` + honest doc.
Compiler-driven rename hit exactly the ResolvedPackage sites; the
Lua-visible "commit" record key is unchanged.
F-012 (Low) — the topo sort built an indegree map, argued in comments it
was backwards, and rebuilt it. Delete the dead first block + the
meandering narration.
Framing/as-built: docs/package-manager-hardening-framing.md.
Validated: fmt clean; clippy --all-targets clean under both Lua flavors;
1436 lib unit tests pass (incl. new F-005/F-009 tests, the F-010 timeout
test, and the loader override test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
The core-side machinery the menu and clipboard ride on, plus the Lua
resolver. Still no dispatch wiring (that needs the protocol/frontend
commit), so this builds but nothing is reachable yet.
- Clipboard (Q#CM6): an in-core slot + `copy`/`cut`/`paste`/`select-all`
on `EditorCore`, plus a one-shot `pending_clipboard` the dispatcher
will drain. `region_bytes` / `word_at_cursor` (the latter feeds the
`symbol` context).
- Menu core (Q#CM1): `SharedMenu` field + `menu_open/close/step/
set_active_row/active_command/hit` + `ensure_menu_overlay`.
- `pmacs.menu` install (item/list/remove/clear/_raw) and `ed.*` bindings
(clipboard_copy/cut/paste, select_all, word_at_cursor); the `install`
signature gains the menu registry, threaded through `lua.rs`.
- `builtin/menus/default.lua`: `pmacs.menu.build` resolves visible items
(predicate or context tag), groups/sorts, and emits rows (Q#CM3). The
default items reference commands by name (resolved at invoke).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Wires regex matching into the search session and the terminal
frontend.
SearchSession gains `regex` and `invalid` flags. search_begin takes a
`regex` argument; recompute dispatches find_all_regex (regex) vs
find_all (literal), recording `invalid` when the pattern won't
compile (an invalid pattern clears the matches and shows [invalid]
rather than a stale count). search_toggle_regex flips the mode and
re-runs the current query.
Input: C-M-s / C-M-r start a regex search (search.forward-regex /
search.backward-regex commands → ed.search_start(forward, regex)).
M-r toggles literal <-> regex mid-search — a new SearchKey decoded in
dispatch_search_key, so it works the same in both frontends (the GUI
already round-trips every key while searching). The TUI prompt reads
"Regex I-search:" in regex mode and "[invalid]" when the pattern
won't compile.
Multi-line: SearchView now washes each row a match spans, mirroring
paint_local_selection's per-row clip (newline excluded so a spanning
match doesn't paint a phantom trailing cell). Single-line matches —
every literal match — touch exactly one row, unchanged. The GPU
already fans multi-line ranges per-line, so it needs no change here.
Tests: regex match / smart-case / invalid-flags-and-recovers /
toggle-reinterprets-query (core); C-M-s starts regex + M-r toggles
mid-search (dispatch); multi-line per-row wash (SearchView render).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the live-typing half of in-buffer search for the terminal
frontend, on a frontend-agnostic core so the GPU (next commit) can
share it.
EditorCore gains a `search: Option<SearchSession>` (query + origin
cursor + direction) and the `search_*` methods that drive it:
begin records the origin, input_char/backspace re-run `find_all`
against the origin buffer and refocus the match nearest the origin
(failing searches anchor the cursor back at the origin), step walks
the store's active match (wrapping, also usable post-accept), and
finish either keeps the cursor + matches (accept) or restores the
origin and clears them (cancel). The matches live in the shared
`search_store`, so the decorations producer and the TUI SearchView
light up live as you type.
Input routing is intercepted in `EditorState::dispatch_key`: while
a search runs, every key flows through `dispatch_search_key`
(SearchKey::from_chord) instead of the global keymap — printable
chars extend the query, C-s/C-r (and Down/Up) step, RET accepts,
C-g/Esc cancel, BS shortens. This is the same dispatch path the
daemon's `FrontendEvent::Key` round-trip uses, so the daemon-side
search already works; the GPU just needs to route keys + show the
prompt (commit 4). The TUI paints an `I-search: <query> (n/m)`
prompt on the bottom row while keeping the terminal cursor in the
buffer at the active match.
C-s / C-r start the search (search.forward / search.backward Lua
commands → ed.search_start). Both keys were free in the default
map (save is C-x C-s, redo is C-x r), so isearch lands without
disturbing the CUA / Emacs editing keys — no cursor.right rebind
needed (the framing doc had flagged C-f for veto; C-s is cleaner
and Emacs-faithful).
Any edit now marks the buffer's matches stale in apply_active_edit
(M11.8), closing the headline "stale-after-edit linger" bet:
accepted highlights vanish the moment the text they described
changes, rather than painting at wrong offsets.
Tests: EditorCore-level (begin/type/step/wrap/focus-from-origin/
cancel/accept/backspace/smart-case/stale-on-edit) and dispatch-
level acceptance (C-s drives the whole loop; Esc restores; query
keys never self-insert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typing over a selection composed two edits in Lua —
delete_region() + insert_char() — so it recorded two undo steps:
one undo left the half-replaced text, two restored the original.
Undo granularity is per apply_edit in both modes (v0.1 pushes one
UndoEntry per edit; CRDT commits per edit via export and groups by
commit, with record_checkpoint unused), so the fix is to make
type-over one edit.
New core EditorCore::insert_char_over_region emits a single
EditOp::Replace when a region is active (cursor past the inserted
bytes, selection cleared) and delegates to insert_char otherwise.
The three type-over commands (buffer.newline / tab / self-insert)
call it via a new Lua binding instead of the delete+insert pair.
delete_region and insert_char are unchanged for their other
callers; region-aware backspace/delete already emit one op.
Verified one undo unit in BOTH modes (dual_mode
replace_is_a_single_undo_step covers v01 + crdt — a CRDT Replace is
delete-then-insert internally but one commit) plus an end-to-end
acceptance test through the key-dispatch path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M4.6 follow-up piece 2. `Style` gains `underline_color: Color`
(Default = follow the text color) so a diagnostic squiggle can be
red/yellow/cyan/gray without clobbering the syntax color of the
text it underlines — exactly why error_style() left its 'red'
unwired until now.
The wire consequence: Style rides inside Cell / CellDelta /
Snapshot / StyleSpans, so this is the protocol's first
encoding-breaking change. PROTOCOL_VERSION 5 → 6 and
SUPPORTED_PROTOCOL_VERSIONS narrows to [6]: postcard is not
self-describing, so no per-session send gate can keep a v5 peer
decoding v6 cells — a mismatched pair now fails the handshake with
a clean VersionMismatch instead of garbling mid-session. Version
policy tests rewritten to pin the new contract.
Surface wiring:
- diag.rs: per-severity underline_color (indexed 1/3/6/8).
- frontend.rs: kitty-style CSI 4:N for Double/Curly/Dotted/Dashed
(previously flattened to plain SGR 4) + SGR 58:5/58:2 emission.
- ansi.rs: parse SGR 58/59 with the 38/48 extended-color grammar.
- overlay.rs merge_styles: non-default-wins, like fg/bg/underline.
- lua_bindings.rs: underline_color on Lua style tables.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-document didChange went out per keystroke: three O(file) copies,
O(file) JSON, and a BLOCKING pipe write on the daemon main thread
(Linux pipe buffers are 64KiB; a 240KB notification stalls the frame
loop until the langserver drains). The dominant daemon-side typing
cost on large files, and freeze-class when a server stops reading.
- lsp.lua: the after-edit hook now bumps the version, marks the
cached render families stale (new _mark_document_stale binding, so
stale suppression stays keystroke-accurate), and records the buffer
dirty. The coalesced send fires on the async tick after 75ms of
quiet, or at most 400ms behind during continuous typing. Anything
that consults the server flushes first (attached_for_active,
repull_for_attachments, pull_inlay_hints_quiet) so requests and
position-encoding conversion never see stale text. Versions may
skip values; LSP only requires they increase.
- Inlay hints re-pull at flush cadence: they're pull-model, nothing
re-requested them after edits, so hints died on the first
keystroke and never returned.
- process.rs StdinWriter: a per-generation writer thread owns the
child's stdin; write_stdin queues and never blocks (64MiB budget
converts a wedged child into an error); close_stdin drains then
EOFs, preserving the MCP flush-then-EOF contract.
- pmacs.editor.monotonic_ms + pmacs.lsp._flush_did_changes bindings;
acceptance test pins burst-coalescing, flush-on-demand, and the
quiet-window tick flush.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TUI's `DiagnosticView` has existed in `src/diag.rs` since v0.1 but
was never instantiated, so the local-grid renderer never painted
diagnostic underlines. This wires the view in the same way
`LspStyleView` and `SyntaxHighlightView` are wired — a Lua binding
that pushes the overlay onto the active window, driven from
`lsp.lua`'s `attach_buffer` flow with the standard per-buffer dedup
table.
* `DiagnosticView::kind()` returns `"diagnostic"` so
`pmacs.window._overlay_kinds()` can verify attachment.
* `pmacs.diag._attach_view(buf, uri)` mirrors `pmacs.lsp._attach_style`
exactly: requires active window's buffer matches `buf`, constructs
`DiagnosticView::new(uri, store)`, pushes as overlay.
* `lsp.lua` calls `pmacs.diag._attach_view` from `attach_buffer` and
tracks pushed buffers in `diag_viewed_buffers` to prevent
double-attach on repeated `attach_buffer` calls.
Scope is intentionally narrow: view attachment only. Navigation
bindings, statusline summary, and gutter signs remain follow-ups
under task #23.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Closes the visible "C++ has no syntax coloring in the grid TUI" gap.
Sibling of SyntaxHighlightView: a View impl that paints LSP semantic
tokens as cell styles, attached for buffers with no bundled
tree-sitter grammar. Same policy A (one styling authority per buffer)
the semantic-frontend producer arc enforces, applied to the grid
renderer the user actually uses today.
Mechanics: every render re-derives the buffer's URI from
buf.file_path() and pulls (encoding, legend) via the existing
LspManager::semantic_style_context plus tokens via for_uri. Per
visible line, tokens are converted from LSP encoding units to byte
ranges via char_to_byte, then to display columns via the existing
byte_range_to_display_cols (UTF-8 + tab aware). Theme::lookup
resolves token type names through the same dotted-prefix mechanism
the tree-sitter capture names use, so "function", "variable",
"type", "keyword" land on the existing theme vocabulary with no new
style names. Default-styled spans skip the per-cell loop, matching
SyntaxHighlightView's short-circuit.
Wiring: pmacs.lsp._attach_style binding pushes the overlay on the
active window (mirrors pmacs.parse._attach_highlight). install_lsp
and make_lsp_manager take SharedSyntaxRegistry so the binding can
hand the LspStyleView the shared ThemeHandle; editor.rs caller
updated. builtin/runtime/lsp.lua's attach_buffer attaches the view
when pmacs.parse.language_for_path returns nil (grammar-less
signal), dedup'd via a styled_buffers set that mirrors syntax.lua's
highlighted_buffers.
Test: lsp_style_view_paints_cells_from_semantic_tokens — seeds an
Initialized fake LSP client (using the cfg(test) helper from the
producer arc) on a /tmp/x.cpp buffer with one token, asserts the
expected cells are styled per the theme face and the cell just past
the token range is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dynamic file-watch registration with a full snapshot-diff watcher.
- src/lsp.rs: did_change_watched_files(sid, &changes) notification;
capability workspace.didChangeWatchedFiles.dynamicRegistration=true
(mandatory — clangd/rust-analyzer/gopls only register dynamically).
- src/lua_bindings.rs: pmacs.lsp.did_change_watched_files binding.
- builtin/runtime/lsp.lua: client/(un)registerCapability handled in
the server-request pump (reply null; start/stop watchers). Brace-
expanding glob → anchored Lua pattern; recursive read_dir/stat
snapshot-diff poller emitting per-file created/changed/deleted
filtered by glob + WatchKind, batched into one notification;
self-cancels when the server dies or unregisters. luajit-safe
(kind_has() arithmetic, no 5.3 bitwise).
- pmacs_fake_lsp.rs: `filewatch` mode registers a **/*.txt watcher
and logs received changes to <base>/.received (disk side-channel —
the protocol stream is drained by the pump).
- tests/m4_acceptance.rs: m4_24 asserts create(1)/change(2)/
delete(3) for matching .txt only; non-matching .md filtered.
Bug caught in validation: `**/` → `(.*/)?` is not a valid Lua
pattern (no group quantifier) — matched nothing, zero events. Fixed
to `**/`→`.-`, `**`→`.*`; m4_24 surfaced it.
client/unregisterCapability cancels watcher records (code-reviewed);
not asserted in m4_24 — a "no further notifications" negative-timing
check is flaky; the create/change/delete + filter path is the
deterministic proof.
Gates: lib 1301/0, m4 79/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy
clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backlog item 2 — perf refinement over the v1 full-only request.
- src/semantic_tokens.rs: SemanticTokensResponse retains the raw
int stream; factored decode(); new apply_delta(prev_raw, v)
splices a SemanticTokensDelta (edits:[{start,deleteCount,data}])
over the previous raw — descending-start application so unordered
server edits stay valid, bounds clamped, spec-allowed
full-instead-of-delta detected and parsed. +4 unit tests.
- src/lsp.rs: request_semantic_tokens_range (reuses the
SemanticTokens route/store) and request_semantic_tokens_delta
(new ResponseRoute::SemanticTokensDelta; absorb splices against
the store's retained raw). Capability upgraded to
requests:{ full:{ delta:true }, range:true }.
- src/lua_bindings.rs: _request_semantic_tokens_range_raw,
_request_semantic_tokens_delta_raw,
pmacs.semantic_tokens.result_id(sid,uri).
- builtin/runtime/lsp.lua: range/delta wrappers;
pmacs.lsp.semantic_tokens() auto-prefers delta when a prior
result id exists (else full), no longer clears the store (delta
needs the retained raw), tags the modeline "(delta)". The range
wrapper is exposed without a default command (no viewport source
in the bundle yet).
- pmacs_fake_lsp.rs: /range and /full/delta arms (delta is an
edit script over the /full data).
- tests/m4_acceptance.rs: m4_20 (range decode), m4_21 (full seeds
rid-1; delta against it splices to the updated 3rd token + rid-2).
Gates: lib 1289/0, m4 76/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy
clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LSP data layer only — independent of the M11 semantic-render
protocol (semantic_render.rs / semantic_client.rs, tree-sitter →
frontend wire families). No InstanceMessage family added; wiring
LSP tokens into styling is a separate rendering milestone. Same
shape as every sibling LSP feature: typed store + async request
+ Lua surface + command + modeline summary.
- src/semantic_tokens.rs: decode the 5-int relative encoding
(deltaLine, deltaStartChar, length, tokenType, tokenModifiers)
into absolute SemanticToken{line,start,length,token_type,
token_modifiers}, with the same-line-vs-new-line deltaStartChar
rule and defensive truncation of a malformed trailing group.
SemanticTokensLegend::from_capabilities parses
semanticTokensProvider.legend and resolves type index / modifier
bitset to names. Store keyed (server, uri). 6 unit tests.
- src/lsp.rs: store + accessor, ResponseRoute::SemanticTokens +
absorb, request_semantic_tokens (/full; v1 no range/delta),
textDocument.semanticTokens client capability (full-only,
formats=[relative], standard LSP legend).
- src/lua_bindings.rs: _request_semantic_tokens_raw,
pmacs.semantic_tokens.{tokens, legend, clear} (legend reads the
per-server initialize capabilities).
- pmacs_fake_lsp.rs: semanticTokensProvider.legend in initialize;
textDocument/semanticTokens/full arm with relative-encoded data.
- builtin/runtime/lsp.lua: pmacs.lsp.semantic_tokens() requests
full, stores, modeline summary (first token's type resolved via
legend); lsp.semantic-tokens command + C-c y.
- tests/m4_acceptance.rs: m4_17 drives the request via the Lua
surface, asserts decoded absolute tokens (incl. deltaLine!=0 ⇒
absolute startChar) and legend index→name resolution.
Gates: lib 1285/0, m4 72/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0 (confirms no
collision with the M11 render protocol); fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Independent LSP feature (not part of the L1-L4 cross-file arc),
shipped in the same shape as every sibling: typed store + async
request + Lua surface + command + modeline summary, with the
inline renderer deferred as its own milestone.
- src/inlay_hint.rs: parse InlayHint[]|null — position, label
(string OR InlayHintLabelPart[] flattened), kind (type/
parameter), paddingLeft/Right, tooltip (string|MarkupContent).
Store keyed (server, uri). 5 unit tests.
- src/lsp.rs: inlay_hint_store + accessor, ResponseRoute::InlayHint
+ absorb, request_inlay_hint (range params), textDocument.
inlayHint client capability (no resolveSupport/refreshSupport —
full hints, on-demand re-query is the v1 model).
- src/lua_bindings.rs: _request_inlay_hint_raw,
pmacs.inlay_hint.{hints,clear}.
- pmacs_fake_lsp.rs: textDocument/inlayHint arm returning a
string-label type hint and a label-parts parameter hint.
- builtin/runtime/lsp.lua: pmacs.lsp.inlay_hints() requests over
the whole-buffer range, stores, modeline summary;
lsp.inlay-hints command + C-c i; scope header notes the inline
renderer is a later milestone.
- tests/m4_acceptance.rs: m4_16 drives the request via the Lua
surface, asserts both label shapes / kinds / padding parsed.
Deferred (scoping, not a regression): inline virtual-text
rendering. The VirtualCellOverlay model only overwrites existing
cells; rendering hints inline needs a column-inserting/reflowing
renderer — a rendering milestone, not an LSP task — staged like
the hover panel / references list. pmacs.inlay_hint is the data
surface a future render layer subscribes to.
Gates: lib 1279/0, m4 71/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy
clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Final cross-file layer: filesystem resource operations in
documentChanges, applied in server order alongside text edits.
- src/rename.rs: replace the files/unsupported_ops split with a
single ordered Vec<WorkspaceOp> (Edit | Create | Rename | Delete
with options). Order preserved exactly as sent so create-before-
edit works; the `changes` map still emits URI-sorted edit ops.
files()/is_empty()/edit_count()/resource_op_count() helpers.
Tests reworked to the ops model.
- src/code_action.rs: adapt to the ops model (has_edit unchanged).
- src/lua_bindings.rs: workspace_ops_to_lua (ordered, op-tagged) +
file_edits_to_lua (back-compat); pmacs.rename.ops;
_parse_workspace_edit -> { ops }; code-action edit is ops; new
pmacs.buffer.apply_resource_op doing the filesystem op plus
buffer-registry reconciliation (rename rebinds an open buffer's
path; delete removes its buffer; create makes parent dirs and
honours overwrite/ignoreIfExists).
- builtin/runtime/lsp.lua: apply_workspace_edit rewritten to walk
the ordered ops, preflight-resolve every URI before mutating
anything, run text edits via apply_text_edits and resource ops
via apply_resource_op, restore origin best-effort. Returns
edits, files, resource_ops; status messages updated.
- pmacs_fake_lsp.rs: drop the stray /tmp create from `rename`
mode; add a `resourceops` mode whose executeCommand->applyEdit
returns create -> edit-created -> rename -> delete.
- tests/m4_acceptance.rs: m4_15 drives all four ops through the
applyEdit pump and asserts disk effects + create-before-edit
ordering.
Gates: lib 1274/0, m4 70/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m11_5 (--features crdt) 2/0; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Third cross-file layer: code actions and the server→client edit
channel that executeCommand-driven actions depend on.
- src/code_action.rs: normalise (Command | CodeAction)[] into one
CodeActionItem (bare-Command vs nested-Command disambiguated by
whether top-level `command` is a string; inline `edit` reuses
rename::WorkspaceEditResponse). Store keyed (server, uri). 5 tests.
- src/lsp.rs: code_action_store + accessor, ResponseRoute::CodeAction
+ absorb, request_code_action, request_execute_command (awaiter
only — effect arrives out of band). Capabilities: codeAction
(+codeActionLiteralSupport), workspace.executeCommand, and
workspace.applyEdit flipped to true.
- src/lua_bindings.rs: _request_code_action_raw,
_request_execute_command_raw, _parse_workspace_edit (any raw
WorkspaceEdit JSON -> applier input shape), pmacs.code_action.*.
- pmacs_fake_lsp.rs: textDocument/codeAction arm (command action
first, inline-edit action second) + workspace/executeCommand arm
that emits a server→client workspace/applyEdit before responding.
- builtin/runtime/lsp.lua: pmacs.lsp.code_actions (apply first
action: inline edit and/or executeCommand); the applyEdit pump
(chained on pmacs._async.tick, drains only attachment-server
events, snapshots server ids before applying since find_or_open
can mutate `attachments`, replies { applied }); lsp.code-actions
command + C-c a keybind.
- tests/m4_acceptance.rs: m4_14 drives the full
codeAction→executeCommand→applyEdit chain end to end.
Gates: lib 1273/0, m4 69/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m11_5 (--features crdt) 2/0; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second cross-file layer on the L1 foundations: a multi-file
WorkspaceEdit applier and an LSP rename UX driving it.
- src/rename.rs: parse a WorkspaceEdit (both `changes` map and
`documentChanges`, the latter preferred per spec; AnnotatedTextEdit
handled; create/rename/delete resource ops counted into
`unsupported_ops` for L4) into per-file TextEdit lists. RenameStore
keyed by the request's origin URI. 6 unit tests.
- src/lsp.rs: rename_store + accessor, ResponseRoute::Rename,
request_rename, and the textDocument.rename client capability
(prepareSupport=false — L2 renames from the cursor position).
- src/lua_bindings.rs: _request_rename_raw + pmacs.rename.{file_edits,
unsupported,clear}.
- pmacs_fake_lsp.rs: textDocument/rename arm; `rename` mode returns a
2-file documentChanges plus a create resource op.
- builtin/runtime/lsp.lua: apply_workspace_edit (preflight rejects
unresolvable URIs before mutating anything; per-file reverse-sorted
application; origin buffer restored), pmacs.lsp.rename with a
minibuffer prompt, lsp.rename command, C-c r keybind.
- tests/m4_acceptance.rs: m4_13 drives rename end-to-end through the
minibuffer and asserts both files mutated + origin restored.
Gates: lib 1268/0, m4 68/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m11_5 (--features crdt) 2/0; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lays the groundwork for WorkspaceEdit/rename (L2+) by making
navigation cross-file-correct.
- Relocate file_path/file_meta from the EditorCore global onto
Buffer itself, so each buffer keeps its own filesystem identity
across cross-file navigation. Accessors + registry/editor/lua/
semantic_render call sites migrated; zero behavioural change for
single-file flows.
- uri->path: project_index::uri_to_path made pub; pmacs.lsp.path_for_uri.
- find-or-open: BufferRegistry::find_by_path + pmacs.buffer.find_or_open
dedups an already-open file instead of spawning a duplicate buffer
(SP-4 Gap A).
- Bounded jump ring on EditorCore (cap 64, oldest-evict, stale-buffer
skip): push_jump/jump_back + pmacs.editor.* bindings + lsp.jump-back
command bound to M-,.
- pmacs.lsp.go_to_definition cross-file branch: decode URI ->
push_jump -> find_or_open -> reposition, with a failure path that
unwinds the pushed origin. ensure_server now passes cfg.env through.
Tests: 5 jump-ring unit tests; m4_12_cross_file_go_to_definition_and_
jump_back end-to-end via a new `defenv` fake-LSP mode. All gates green
(lib 1262/0, m4 67/0, m8_1/m8_9/m8_10, m9_1, m11_5 --features crdt 2/0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Completes the read-only LSP feature set (everything except edits).
Same request→store→Lua pattern as the nav batch; these three need
new parsing (new response shapes), so they were deliberately split
from the Location-family PR (#16).
- src/symbol.rs: one flat Symbol type for both symbol requests.
from_lsp_value handles BOTH LSP shapes — hierarchical
DocumentSymbol[] (flattened with depth + parent chain) and flat
SymbolInformation[]/WorkspaceSymbol[] (location.uri, range
optional for WorkspaceSymbol). Scope-keyed (Document(uri) vs
Workspace(query)) so an outline and a query don't collide.
- src/document_highlight.rs: range + DocumentHighlightKind (absent
defaults to Text=1 per spec), (server,uri)-keyed.
- lsp.rs: three ResponseRoute variants + absorb arms + request
methods. documentSymbol/documentHighlight ranges convert via the
requested-doc codec; workspace/symbol results are cross-file →
route uri "" → non-destructive passthrough (same rule as
cross-file definition).
- Lua: raw bindings + pmacs.document_symbol / .workspace_symbol /
.document_highlight read surfaces (the new LSP Symbol is aliased
to avoid the pre-existing project_index::Symbol name clash);
lsp.lua wrappers + an lsp.document-symbols command on C-c o
(modeline summary; outline buffer is future UX).
- Tests: 6 parser unit tests (hierarchical depth/parent, flat
SymbolInformation, range-less WorkspaceSymbol, highlight kind
default, scope non-collision) + an e2e driving all three through
the async bridge asserting shape correctness.
Also includes a pre-existing rustfmt normalization of the #15
semantic-frontend files (protocol.rs / semantic_client.rs /
semantic_render.rs) — main was not rustfmt-clean there after the #15
merge; bundled here per operator decision so the fmt gate is green.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1257/0; m4_acceptance 66/0; m9_1 18/0; m8_1/m8_9/m8_10 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The "cheap batch" subset that is genuine template-fill: these four
return the exact `Location | Location[] | LocationLink[] | null`
shape `textDocument/definition` already parses, so no new parsing —
only a kind discriminator so they don't collide on (server, uri).
- src/locations.rs: a (server, uri, kind)-keyed store whose value
type is the reused crate::definition::DefinitionResponse. The
proven definition store + Lua API are untouched.
- lsp.rs: ResponseRoute::Locations { uri, kind }; one absorb arm; a
DRY request_locations helper + request_references /
request_declaration / request_type_definition /
request_implementation. references sends
context.includeDeclaration. Supersede keys derive from each
kind's distinct method, so the four don't cancel each other.
- Lua: _request_*_raw bindings + install_locations exposing
pmacs.references / .declaration / .type_definition /
.implementation ({ locations, clear }, mirroring pmacs.definition,
reusing definition_response_to_lua). lsp.lua Handle wrappers + a
lsp.find-references command bound to M-? (modeline summary;
references-list buffer is future UX, like the hover panel).
- Tests: locations.rs unit tests (kind labels distinct; keys don't
collide); e2e driving all four through the async bridge and
asserting each routes to its own kind slot (fake returns distinct
lines 11/21/31/41).
Scoped: documentSymbol / workspaceSymbol / documentHighlight return
different shapes (new parsing) — a separate follow-up, not crammed
in here.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1232/0; m4_acceptance 65/0; m9_1 18/0; m8_1/m8_9/m8_10 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gopls / pyright / basedpyright / clangd issue server→client
`workspace/configuration` during startup and degrade (or fall back
to defaults) without a reply. pmacs advertised `configuration:false`,
so it never got the chance.
- Advertise `workspace.configuration: true`.
- New `settings` field on the spawn spec, threaded through
lua_to_lsp_spec → ensure_server (pmacs.lsp.config[lang].settings).
- handle_request intercepts `workspace/configuration` (mirrors the
publishDiagnostics interception in handle_notification): each
item's dotted `section` resolves against the server's settings via
resolve_config_section; one array element per item; unknown
sections answer `null` (the spec's "not configured" signal,
distinct from a configured null). All other server→client requests
still surface as a `Request` event for the consumer.
- The Python default now ships
`python.analysis.typeCheckingMode = "basic"` (+ basedpyright.*
alias), so the #12 basedpyright-noise concern is now actually
fixed rather than only documented; a project pyrightconfig.json /
[tool.pyright] still wins where present.
Scoped: `scopeUri` ignored (single-root; same settings regardless
of scope) until multi-root, a separate deferred item.
Tests: exhaustive resolve_config_section unit test (dotted paths,
configured-null vs unknown-null, whole-object for absent section);
new `wsconfig` fake mode pulls config at `initialized` and echoes
pmacs's answer back; end-to-end test asserts the configured section
round-trips.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1230/0; m4_acceptance 62/0; m9_1 18/0; m8_1/m8_9/m8_10 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the editor-blocking `poll_until` tick-loop in the LSP UX
with the M9.1 external-settle pattern: each `textDocument/*` request
registers a pending entry via `AsyncRuntime::register_external` and
returns the job id; the JSON-RPC response (or a server-teardown /
cancel / timeout) settles it, resuming a `Handle:await()` coroutine.
No worker thread is occupied for the round-trip.
Hybrid result delivery (operator decision): the response is absorbed
into the typed stores *and* carried through the Handle. The
completion popup and diagnostics gutter keep reading the stores
untouched; request/response command code awaits the value directly.
Core (src/lsp.rs):
- `LspManager` gains `runtime: SharedAsyncRuntime` (threaded through
`make_lsp_manager` / editor.rs, mirroring `make_mcp_manager`) plus
a `(server, request_id)` -> PendingExternal awaiter table parallel
to `pending_routes`.
- `request_*` return the async `JobId` (`= u64`, signature
unchanged; no caller consumed the old JSON-RPC id).
- `handle_response` settles every non-cancelled awaiter ok/failed
alongside store absorption; null result still wakes await with nil.
- Awaiters drain-cancelled at all three `pending_routes` purge sites
(restart generation flip / terminal exit / forget) so a coroutine
cannot park on a server that went away.
- Per-tick sweep: per-awaiter cancellation (Handle:cancel() or
supersede via a stable `lsp:{method}:{sid}:{uri}` key), with
`$/cancelRequest` + `cancelled_rids` on abandonment to drop the
cancel/response race silently. Mirrors mcp.rs.
- Per-request timeout (default 10s, `pmacs.lsp.set_request_timeout_ms`):
an alive-but-silent server fails the await instead of hanging.
Lua surface:
- `_request_*_raw` job-id bindings (mirror `pmacs.mcp._send_request_raw`).
- builtin/runtime/lsp.lua: Handle wrappers + the four commands
rewritten to spawn `pmacs.async` coroutines that `:await()`;
`poll_until` removed. Server-gone / error surface as structured
await failures in the modeline.
Tests:
- pmacs_fake_lsp: `error` / `silent` modes for deterministic
failure-path coverage.
- 5 end-to-end await-path tests (success+store, server-error->failed,
server-stop->cancelled, timeout->failed, supersede->cancelled).
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1223/0; m4_acceptance 58/0; m9_1_acceptance 18/0 (MCP unaffected).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pulls a set of planned V0.2 prerequisites forward to ship in v1.0,
plus the clean audit-review round over that work.
V0.2-prereq implementations (documented promotions, not M11
surprises; operator raised the v1.0 public-API ceiling to absorb
them — see V0.2-PREREQUISITES.md "v1.0 pull-forward"):
- CC-1: `bypass_intercept` opts on buffer insert/delete/replace —
skips the Lua intercept chain only; preserves the same-buffer
re-entry guard, undo/dirty bookkeeping, view notifications, and
CRDT broadcast queueing.
- CC-2: `pmacs.buffer.on_removed(buf, cb)` + idempotent `:remove()`
handle; buffer-local keymaps pruned on removal. Fires for both
`pmacs.buffer.remove` and `.kill` (incl. interactive C-x k);
callback errors logged to *errors* without failing the removal.
- SP-4: `pmacs.buffer.from_file`.
- SP-5: `pmacs.fs.watch` (polling; `:cancel()`/`:is_cancelled()`).
- SP-7: `pmacs.async.yield_to_next_tick` (worker-free next-tick
yield); outline-aggregate repaint now uses it instead of
workers.sleep(0):await(), pinning propagation to one async tick.
- SP-1: `pmacs.editor.move_to_line` (0-based, clamps out-of-range).
- SP-6: `pmacs.outline.query` published by pmacs-outline.
- SP-3: audit rule 15 `reach-around-require-field` (Info).
- CC-3: runtime API-availability documented (docs-only).
Clean audit-review round (M10.11 framing stop-condition pass):
- Finding 1 (fixed): clippy needless_raw_string_hashes blocked
`clippy -D warnings` on both lanes; raw-string delimiter fixed.
- Finding 2 (fixed): fs_watch acceptance test was racy — the
`pending == 1` gate could not distinguish the in-flight baseline
stat from the steady-state poll sleep, so under load the mutation
raced the baseline (~1/3 fail in the default lane). Rewritten to
re-emit a distinct change each pump iteration; 6/6 on the
previously-failing invocation.
- Finding 3 (fixed): documented fs.watch's async-baseline startup
window and size+mtime-granularity detection limit.
- Finding 4 / SP-8 (logged, non-blocking, out of diff): a
pre-existing PTY-lifecycle test timing flake under severe CPU
oversubscription; src/process.rs untouched here.
CC-1's opts-extension-counts question resolved explicitly
(consistent treatment: counted; ceiling raised to fit).
Gate at normal load, both lanes: fmt clean; clippy --all-targets
-D warnings clean; non-crdt lib 1223/0; crdt lib 1377/0;
m8_1/m8_9/m8_10 green.
Not in scope here: v1.0 CHANGELOG body, version bump, the M10.11
Finding-4 (reattach undo) user-facing artifact, and the recorded
two-laptop manual acceptance — tracked as the remaining v1.0 steps.
.gitignore: M*-FRAMING.md added to the internal-only block for
consistency with the M*-AUDIT.md / M*-SHIP-GATE.md siblings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the optimistic local-edit-application layer on top of the M10 CRDT
foundation: frontend-side rope replica with local edit application,
daemon-authoritative broadcast, and bidirectional cursor reconciliation.
Keystrokes feel instantaneous because the local replica answers next-render
queries before the daemon round-trip completes, while the daemon remains
the single source of truth for conflict resolution and broadcast to remote
replicas.
Architecture beats:
- BufferMirror (src/buffer_mirror.rs) holds a per-frontend rope replica
with explicit cursor-staleness tracking. Every event that may move the
active cursor or swap the active buffer marks the mirror stale; the
next CursorByte from the daemon clears it.
- CrdtOpOrigin {OptimisticReplica(FrontendId), DaemonKey} routes broadcast.
OptimisticReplica skips re-application on the originating frontend
(already applied locally); DaemonKey broadcasts to all replicas including
source -- covers Lua-driven and generated-buffer edits that bypass the
optimistic path.
- Generated buffers (*help*, *workers*, *pmacs-instance*, *errors*) funnel
apply_edit output through queue_daemon_origin_crdt_op so post-attach
CRDT upgrades don't drop their edits.
- forbid(unsafe_code) preserved throughout; loro 1.12 added as the CRDT
engine.
Audit posture: M10.10 shipped through six post-audit review rounds with
twenty-eight cumulative findings, most categorized as "incomplete
application of a prior round's mechanism." The audit doc records
grep-driven exhaustiveness as the standing countermeasure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the Model Context Protocol (MCP) integration as a transport binding,
not a built-in feature. Six Lua functions plus userdata methods expose
the substance of three MCP feature areas (resources, tools, prompts), a
notification dispatcher, and a non-trivial AI-assistance example package
that meets the architectural ship gate (spec/pmacs-spec.tex:1572): zero
direct calls into the Rust core, zero special-cased MCP handling outside
the public API, source under 2000 lines of Lua.
The M9.5 -> M9.6 -> M9.7 -> M9.8 layered composition validates the claim
"AI is a transport binding, not a feature" -- pmacs-mcp-ai composes with
pmacs-mcp-prompts.render and inherits notification handling transitively
through M9.7's package, demonstrating that the AI domain is a layer
above MCP, not a thread woven through the core.
Subtask shape:
M9.1 stdio transport + initialize handshake + restart policy
M9.2 resources with in-flight + settled cache and per-uri invalidation
M9.3 tools with isError-vs-JSON-RPC-error semantics + cancellation
M9.4 prompts with required-argument validation
M9.5 notification dispatcher (on_notification, off_notification)
M9.6 tools-as-commands fixture package + 12 audit findings disposed
M9.7 prompts-as-result-buffers fixture package + tree-sitter-md grammar
M9.8 AI-assistance fixture package (363+ LoC; 17/17 acceptance tests)
M9.9 formal package audit -- PASS on all three criteria
M9.10 release: TRANSITION-M9.md + MCP-for-package-authors guide
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>