Three PR #96 review findings (documentation accuracy):
- Q#P7 coordinates section claimed panels inherit a byte==UTF-16 wire
assumption with position-encoding hardening deferred. False as-built:
the transport layer negotiates general.positionEncoding and converts
every Position at the request/response boundary (PositionEncoding +
char_to_byte/byte_to_char, src/lsp.rs), so location rows reach Lua as
byte offsets. Reworded to record what landed; the true residual is
the codepoint-vs-byte cursor walk in move_active_cursor_to (shared
with go_to_definition, not introduced by panels).
- Intro described pre-arc behavior in present tense (references throw
rows away, code actions apply acts[1] blind, ...). Marked as the
pre-arc baseline with a status banner + inline as-built pointers.
- Drifted hard-coded line refs (editor_core.rs:2052-2071,
lsp.lua:658-662, lsp.lua:1187-1213) replaced with symbol names.
Also fixed the move_active_cursor_to comment in lsp.lua itself — it
was the same 'v0.2 hardening' false trail the doc's stale ref pointed
at, now naming the real residual (codepoint-walk, not wire encoding).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the too-many-lines clippy deny the previous commit shipped with
(masked locally by a swallowed exit code in the gate chain); the
shared open_against_fake helper also de-duplicates the two new tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #95 review P3: the new panel paths had no direct coverage. Two
end-to-end tests against the fake server's canned responses:
- outline_panel_opens_visits_and_restores: depth-indented rows with
kind tags, n + RET visits inner's selectionRange (3,7) in the
source buffer, M-, returns to the outline row, q restores.
- hover_doc_panel_shows_full_contents_via_binding: driven through the
REAL C-c H chord (Char('H') + SHIFT through the dispatcher) --
doubling as the shifted-letter binding's parse check, which passes
-- multi-line contents render, q restores.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure Lua on the phase-1 substrate (framing Q#P5).
Outline: lsp.document-symbols (C-c o) opens *outline* -- the store's
FLAT symbol rows indent by their depth field with an LSP SymbolKind
tag; RET pushes the jump ring, restores the source buffer, and moves
to the symbol (M-, returns to the outline row, the references-panel
semantics).
Code actions: lsp.code-actions (C-c a) applies a single action
directly (previous behavior, now correct instead of lucky) and opens
the minibuffer dropdown when several are available -- 'N: title'
candidates; a bare typed index also accepts. The apply branch is
extracted as apply_code_action, shared by both paths. The m4_14/m4_15
acceptance tests (written against blind-first-apply; the fake LSP
returns two actions) now drive the picker: pump until the prompt is
live, type '1', RET -- same command-only action as before.
Hover doc: new lsp.hover-doc (C-c H) renders the full multi-line
hover contents into a non-visitable *lsp-help* panel; lsp.hover
(C-c h) keeps its one-line echo-area summary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 regression (TUI could not attach): the follow path fired for
every crdt_replica session, and its first-tick send delivered a
GUARANTEED duplicate BufferSnapshot right after the attach sweep. The
grid TUI's BufferMirror is init-once -- the duplicate errored ('buffer
already has a CRDT snapshot applied') on every attach, and every TUI
buffer switch would have produced another. Display-follows-snapshot is
a grid-less-frontend concept: the GPU rebuilds its replica wholesale
on every snapshot and is the only consumer that needs the follow. The
send is now gated on negotiated_capabilities.semantic_render -- TUI
sessions get zero follow-sends, byte-identical wire behavior to main.
Verified: PTY-doubled real-binary attach tests pass with the gate;
the one failing PTY test (m10_11_doubled_pty_fixture_propagates_
keystrokes_from_both_sides, 'AXYB' interleaving) fails identically on
main 3/3 -- pre-existing, tracked separately, not this branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Q#P1-P7: panels are buffers (a shared listview runtime module
generalizing the *buffer-list* idiom — zero protocol change, both
frontends render them for free); switch-in-place presentation with q
restore (the GPU cannot show splits); read-only via intercept with its
limits recorded; the Q#P6 round-trip buffer seam so semantic frontends
never optimistic-apply into panels (RET visits instead of inserting a
newline); references list, outline, minibuffer code-action picker,
hover-doc panel; byte==UTF-16 caveat inherited, not multiplied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two pre-merge review findings on PR #93:
1. (High) The BufferSnapshot arm switched current_buffer_id and
dropped every other buffer-local mirror but left self.completion
intact -- and the producer's first-sight-closed silence means no
close message ever arrives for a viewport that no longer exists,
so a stale popup rendered against the new buffer's rope and kept
hijacking Esc/RET/TAB. Fixed three-deep: the snapshot arm clears
the mirror; CompletionLocal now carries its buffer_id; and the
shared completion_open_for_current_buffer() predicate gates both
the key routing and the anchor mapping, so a foreign-buffer popup
can neither paint nor steal keys even if a stale mirror survives
by some other path. Regression: completion_popup_is_scoped_to_its_buffer.
2. (Medium) Optimistic typing (the CrdtOp path, the bulk of GPU
keystrokes) never cleared core.status, so once v15 shipped the
transient message over StatusFacts, '12 references' stayed wedged
in the GPU band through ordinary typing -- only a round-tripped
key's dispatch_key entry clear released it. handle_remote_crdt_op
now clears the status when an edit applies, mirroring dispatch_key.
Regression: handle_remote_crdt_op_clears_the_transient_status.
Also checked: the a_closed_outbox_shuts_the_socket_down... hang seen
once during review did not reproduce in 10 isolated runs -- a
pre-existing timing flake in the F-008 shutdown test, untouched here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validation finding: LSP command summaries ('12 references', hover
first-lines, error reports -- everything pmacs.editor.set_status
writes) showed in the TUI's bottom bar but never in the GPU band,
regardless of which frontend initiated. The attached TUI gets the
message for free through the rendered cell grid's bottom row; a
semantic frontend only sees the wire, and StatusFacts never carried
the message.
Fix inside the still-unreleased v15: StatusFacts gains
message: Option<String> (encoding change to that variant; its daemon
gate moves 8 -> 15, the v10 SearchPrompt / v14 LineNumbers shape --
an old peer's band goes dark rather than mis-decoding). Producer reads
core.status into the cached-compare facts; the GPU band shows the
message echo-area style (under the minibuffer and search prompts,
over the buffer name), returning to the name when the daemon's next
keypress clears it. Producer + postcard round-trip tests added.
The finer-grained results UI (references list, panels, error surfaces)
is Arc 1b on the roadmap; this closes the parity gap until then.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CompletionLocal mirrors the v15 wire (anchor byte, windowed rows,
selection); a close always applies even for a switched-away buffer
(dropping it would wedge a stale popup), while opens follow the CrdtOp
current-buffer rule. Rendering is a dedicated dropdown layer (fourth
TextRenderer + quad batch, the mb_dropdown_* shape): the anchor byte
maps to its glyph rect via the caret walk, rows draw below the anchor
line growing toward the band (flipping above when nothing fits), width
clamps to the window with the left edge shifted back from the right
margin, and the visible slice windows around the selection (F-007
discipline). Kind glyphs replicate the TUI popup's mapping.
Key routing (Q#C6) turned out narrower than framed: C-n/C-p/C-g
already round-trip as command chords and Up/Down as forwarded motion
keys, so only two defaults are wrong under a popup and get gated on
completion_open -- Esc (dismisses via round-trip instead of the local
quit) and RET/TAB (skip the optimistic insert so they accept via
dispatch_completion_key instead of typing a newline/tab). Typing stays
fully optimistic; the daemon's after-edit refresh re-ships the popup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
InstanceMessage::CompletionPopup {buffer_id, anchor: Option<u64>,
prefix_len, rows: Vec<CompletionPopupRow{label, kind, detail}>,
selected, total} -- the first byte-anchored popup on the wire: the
frontend maps byte -> glyph rect locally (the caret precedent), so the
instance never learns a pixel. Rows are display-only; accept resolves
daemon-side via dispatch_completion_key, so insert text never ships.
PROTOCOL_VERSION 14 -> 15, SUPPORTED extended; postcard round-trip
(open + closed shapes) and version-pin/ladder tests updated.
Producer: semantic_render::completion_popup_msg, the family pattern
(per-buffer cached-compare, active-buffer only, first-sight-closed
stays silent) with one new rule -- the session is WINDOW-stamped and
this state is per-frontend, so only the frontend whose own window
owns the session sees it open: a popup opened by TUI typing never
renders in an attached GPU and vice versa. Windowed rows share the
TUI overlay's POPUP_MAX_ROWS. Daemon-gated >= 15 (a v14 peer still
completes via the key round-trip, it just gets no GPU dropdown).
GPU consumption follows in this branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five findings from the manual validation pass, all in-branch:
1. LSP-only words never queried the server: the auto-open path fired
request_completion only when the sync providers already produced
rows. An empty sweep now leaves a pending session and the request
always fires; isIncomplete responses re-request on further typing.
Corollary: attachment_for_request now flushes-if-attached but NEVER
attaches -- the first cut wrapped attached_for_active, which spawns
servers on demand, i.e. per-keystroke spawn attempts in unattached
buffers (wedged the parallel m4 suite; serial ran 3x slower).
Attachment stays buffer-open policy.
2. Cross-buffer LSP leak: the built-in provider's no-uri fallback was
the legacy global store drain, so scratch/unattached buffers could
show another file's cached completions. Strict now: no uri, no rows.
3. Pending prefixes own the keyboard: Action::Pending (C-x ...)
dismisses the popup and the popup shadow is guarded on an empty
dispatcher prefix, so the sequence's continuation and its C-g abort
reach the dispatcher instead of the popup.
4. Window-scoped sessions: CompletionPopupState.window_id (stamped by
completion_popup_open; Lua never sees it). Only the owning window's
overlay paints -- same-buffer splits each carry a persistent
overlay -- and a focus change invalidates the session.
5. Flaky worker test: the /proc thread-count probe and the idempotence
check both build EditorStates and could run concurrently, polluting
the baseline; merged into one test (non-Linux keeps a portable
idempotence variant).
Regression tests for 1-4; framing doc gains the as-built notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every EditorState leaked its entire worker pool (cores-1 threads, each
waking every 100ms): the Rc<AsyncRuntime> is cloned into dozens of Lua
closures, and registries those closures capture store mlua::Function
values -- reference cycles through the Lua VM that keep the Rc from
ever reaching zero, so WorkerPool::Drop never ran. Harmless for one
editor per process; in the m4 acceptance suite (54 editor-building
tests) it accumulated 1000+ live threads (observed: 60 complete
pmacs-worker-0..14 pools at once) plus ~9k spurious wakeups/second.
Fix: WorkerPool::signal_shutdown() -- set the shutdown flag + wake the
parkers from a shared reference; parked workers exit within their
100ms park timeout. Exposed as AsyncRuntime::shutdown_workers(), called
from a new impl Drop for EditorState. Deliberately signal-only, NO
join on the drop path: a worker can be blocked publishing its reply
onto the bus only the main thread drains, and a first cut that joined
in Drop deadlocked the m4 suite at teardown (fake-LSP tests wedged
2h+). WorkerPool::shutdown() (signal + join) remains for owners with
no bus consumer to deadlock against; Drop delegates to it as before.
Measured on the m4 suite: peak live threads 1026 -> 122.
Regression: tests/worker_shutdown_acceptance.rs (thread-count probe is
/proc-based, Linux-gated; the idempotence test runs everywhere).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Q#C2: CompletionPopupState (buffer + byte anchor + prefix + candidates
+ selection) behind SharedCompletionPopup on EditorCore — the
completion twin of SharedMenu. Q#C4: CompletionView reworked from the
dormant M4.7 store-keyed full-viewport painter into a self-positioning
overlay (MenuView model): windows candidates around the selection,
maps the byte anchor to a screen cell via the diag-view walk, places
below the anchor row (flips above when nothing fits), self-suppresses
when closed or on a foreign buffer. Q#C3: dispatch_key gains a PARTIAL
shadow — only TAB/RET/C-n/C-p/Up/Down/Esc/C-g intercept while the
popup is open; everything else falls through so typing keeps
self-inserting, with post-dispatch validation (active buffer, cursor
at/after anchor, word bytes between) closing broken sessions after the
after-edit hook has had its chance to refresh. Q#C7: accept
re-validates at the moment of accept and applies a single Replace
(one undo step; empty-prefix trigger sessions degrade to Insert),
firing buffer.after-edit through the existing revision check.
Framing: docs/in-buffer-completion-framing.md. Lua driver + bindings
follow in this branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
State assessment from a five-way sweep (core editing/persistence, LSP,
GPU parity, extensibility/terminal, deferred-work inventory). Records
the decision to push Arc 1 (completion popup, LSP panels, semantic-token
auto-pull, signature trigger) with Arc 2 editing table-stakes
interleaved, plus the ranked remaining arcs and housekeeping list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrites the stale sections: the intro now names both frontends (TUI +
pmacs-gpu over protocol v14) and CRDT collaboration; Status reflects the
post-1.0 arcs and points at docs/roadmap-2026-07.md; 'What v0.1 ships
with' becomes a current Highlights section (LSP surface, gutter modes,
search, context menu, packages, MCP); adds a Running section with real
daemon/attach/GPU invocations; Layout updated to the three-crate
workspace and the lua_bindings/ module dir. Build feature matrix and
runtime-requirements sections kept as-is (still accurate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three correctness findings from the sub-arc 3 review:
1. (F1) GPU gutter clicks weren't classified before text hit-testing — the
hit path just subtracted `text_left()` and called `buffer.hit()`, so a
click in the gutter band fed glyphon a negative x (undefined) and gave
future gutter markers no stable seam. Extracted `gutter_aware_rel_x`: a
click left of the text origin clamps to `0.0` (the line start), mirroring
the TUI's saturate-to-column-0 affordance. The hit path now branches on
it — the seam a future marker would hook.
2. (F2) The GPU had no fit guard when the gutter consumed the text width.
The TUI drops the gutter for a too-narrow window; the GPU always grew
`text_left()` and `text_bounds_right()` floored against `TEXT_LEFT`, so a
narrow window or very large file could produce `left >= right` (blank /
undefined render). `gutter_width_px` now drops the gutter when it would
leave less than `MIN_TEXT_WIDTH_PX` of text past `TEXT_LEFT`.
3. (F3) The v14 `LineNumbers { mode }` shape had no direct postcard
round-trip (only the version pin + daemon gate). Added one covering all
four `LineNumberMode` variants, so a future enum reorder can't silently
shift the wire.
Tests: `gutter_aware_rel_x` clamps the band (and passes through with the
gutter off); a 60px window drops the gutter while an 800px one keeps it;
all four modes round-trip. fmt + clippy --all-targets clean both flavors +
gpu; 1447 lib + 12 protocol + 57 pmacs-gpu tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Record sub-arc 3: the shared LineNumberMode enum + number_for in
pmacs-protocol, the v14 mode bump, the cursor-line repaint-on-move
dependency, stable gutter width, and the binary-toggle + completion-picker
selection UX. Plus the arc-close note: Q#UX1 scored false (two protocol
bumps), and the deferred gutter-riding features.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Carry the line-number mode to the GPU so it renders relative/hybrid, not
just on/off. The v13 wire carried `LineNumbers { enabled: bool }`
(off/absolute only); v14 carries the full mode.
- Protocol: `LineNumberMode {Off, Absolute, Relative, Hybrid}` moves into
pmacs-protocol (with `number_for`/`is_on`) so the wire, daemon, and both
frontends share ONE enum and ONE number rule (Q#UX7); `pmacs` re-exports
it as `crate:🪟:LineNumberMode`. `LineNumbers.enabled: bool` →
`mode: LineNumberMode`. PROTOCOL_VERSION 13 → 14, SUPPORTED → [6..14],
daemon-gated `< 14` (a v13 peer gets no LineNumbers, like the v10
SearchPrompt bump).
- Producer (`line_numbers_msg`): ships the window's mode (cached-suppress
on the mode now, seeded to Off).
- GPU: `line_numbers` field becomes the mode; `refresh_gutter_buffer`
computes each number via `mode.number_for(line, cursor_line)` against the
GPU's own cursor line (`cursor_line()` off `current_line_starts`). The
buffer rebuilds every render, so relative numbers track the cursor for
free. Gutter width unchanged (sized by line count → stable).
Tests: GPU headless render proves relative ≠ absolute with the cursor on
line 2; producer test asserts the mode ships; protocol version pins → 14.
fmt + clippy --all-targets clean both flavors + gpu; 1446 lib + 12 protocol
+ 55 pmacs-gpu tests pass. Needs a GPU eyeball.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
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
The Up/Down arrows were hardwired to command *history*
(`from_chord`: `Up => HistoryPrev`, `Down => HistoryNext`), so with a
completion dropdown showing they never moved the highlight — only M-n/M-p
scrolled candidates. Pressing Up/Down (the intuitive way) left the
selection stuck, which read as a stuck highlight bar in the GPU minibuffer.
Now Up/Down resolve contextually in the dispatcher (which, unlike the
static `from_chord`, sees the session): when a dropdown is showing they
navigate candidates (`scroll_candidate ∓1`), otherwise they step through
history. C-p/C-n stay pure history; M-n/M-p stay pure scroll. Daemon-side,
so both frontends benefit.
- new `MinibufferAction::{Prev,Next}CandidateOrHistory` (Up/Down); resolved
in `dispatch_minibuffer_key` via `Minibuffer::has_candidates`.
Tests: `from_chord` maps the arrows to the new actions; an end-to-end
editor test opens M-x and asserts Down/Up move the completion selection.
fmt + clippy clean both flavors; 1445 lib tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Record the sub-arc 2 as-built: signs-ride-the-gutter coupling, the
Viewport.gutter_w + paint-reorder mechanism (TUI), the layout_runs bar
mechanism (GPU), the TUI-glyph/GPU-bar rendering difference, the
en-route multi-frontend window.close crash fix (PR #87), and the known
completion-navigation follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
GPU half of sub-arc 2. When the gutter is on, each line carrying a
diagnostic gets a thin severity-colored bar at the gutter's left edge —
the GPU analogue of the TUI's leading-column E/W/I/H sign glyph. No
protocol change: the per-line severity comes from `current_decorations`,
already frontend-side.
- collect_gutter_sign_rects: per visible line (layout_runs), find the
most-severe diagnostic decoration overlapping that line's byte range and
push a `GUTTER_SIGN_W`-wide full-line-height quad at `GUTTER_SIGN_X`,
colored via decoration_kind_to_underline_color. Most-severe wins
(diagnostic_severity_rank; min rank). Gated on line_numbers, mirroring
the TUI (signs ride the line-number gutter).
- The bars ride the existing background quad batch
(decoration_background_vertex_bytes), so no new pipeline.
Headless render test asserts a diagnostic adds ink with the gutter on.
fmt + clippy --all-targets clean; 54 pmacs-gpu tests pass (render tests on
the local adapter). Needs a GPU eyeball before the sub-arc 2 PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Sub-arc 2 of the UX arc, TUI half. When a window reserves a line-number
gutter, lines with diagnostics get a severity-colored sign glyph (E/W/I/H)
in the gutter's leading column — closing the last deferred Task #23 item.
No protocol/daemon change: the per-line severity is already frontend-side
(the diag store the DiagnosticView already reads).
- `Viewport` gains `gutter_w` so overlays can reach the gutter's leading
column at `cell_origin.col - gutter_w`; the text area is already shifted
past it, so viewport-relative painters stay gutter-agnostic.
- The gutter's number pass now runs *before* the overlays (was after), so
the DiagnosticView can draw its sign into the gutter's blanked leading
column without the number pass erasing it.
- DiagnosticView: with a gutter, draw the severity sign glyph colored by
`underline_color()`; without one, keep the legacy column-0 background
marker (the "fake gutter" that predates a real gutter column). Extracted
to `paint_line_markers` to keep `render` under the line cap.
The number never reaches column 0 (>=1 leading pad by construction), so
sign and number coexist. Diagnostic signs currently ride the line-number
gutter (visible when line numbers are on); a signs-without-numbers mode is
deferred.
Test: gutter_sign_replaces_the_column_marker_when_a_gutter_is_reserved.
Validated: fmt + clippy --all-targets clean both flavors; 1441 lib + 22
diag tests pass. Needs a TUI eyeball before the GPU half.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
`close_active` and `close_others` operated on the global `self.windows`
set, which holds *every* attached frontend's windows. With more than one
frontend attached (e.g. a headless `--daemon` plus a pmacs-gpu — two
windows total), closing from one frontend reached across into another's:
- `close_others` did `self.windows.retain(|id| *id == keep)`, deleting the
OTHER frontend's window. Its `view.active` was then dangling and the next
per-tick `active_window()` panicked ("active window present in
core.windows", editor_core.rs:324) — a daemon crash.
- `close_active`'s "only one left" guard checked the global count
(`self.windows.len() <= 1`), so it also proceeded across frontends and
could empty a frontend's layout, panicking on the successor pick.
Both now scope to the active frontend's layout: `close_active` gates on
`active_layout().iter_ids().len()`, and `close_others` prunes only the
active layout's own window ids from `self.windows`.
Regression tests: closing from a second frontend must not remove another
frontend's window, and close-active refuses a frontend's last window even
when other frontends have their own. fmt + clippy clean both flavors; 1442
lib tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
The GPU window opens at 800x200 and the gutter buffer was sized to that
height once at construction. On resize the code buffer is re-sized but the
gutter buffer wasn't, so `shape_until_scroll` only shaped the ~10 lines
that fit the stale 200px height — line numbers stopped at 10 in a
full-height window (the code + every other buffer scaled fine).
Add `gutter_buffer.set_size(width, height)` to the resize handler,
alongside the code/status buffers. The headless path already sizes it
correctly at construction (its window size is final), so its render test
is unaffected.
Validated: fmt + clippy clean; 53 pmacs-gpu tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Fix the control plane for the line-number gutter: M-x
window.toggle-line-numbers now works from EITHER frontend, each affecting
its own window.
Root cause (scores framing bet Q#UX1 false): rendering a gutter is
frontend-local, but the TOGGLE is a daemon command, so the mode has to
reach the GUI over the wire. My earlier GPU control (a --line-numbers flag)
left M-x-in-the-GUI a no-op and the two frontends' settings disconnected.
- Protocol: new additive `InstanceMessage::LineNumbers { buffer_id,
enabled }`; PROTOCOL_VERSION 12 → 13, SUPPORTED grows to [6..13].
Daemon-gated < 13 (a v12 peer keeps its gutter off), like every prior
additive bump — no encoding break.
- Producer: SemanticRenderState::line_numbers_msg reads the frontend's
active window mode (via active_window_for(frontend_id)) and emits on
change; cached-compare suppression seeded to the frontend's `off`
default, so a plain window adds zero traffic and existing frames are
unchanged.
- Daemon: gate LineNumbers >= 13 in the write loop.
- TUI: drops LineNumbers silently (reads its window directly).
- GPU: consumes LineNumbers → drives local `line_numbers`; the
--line-numbers flag retired.
Now the daemon Window.line_numbers is the single source of truth; both
frontends render locally from it.
Tests: line_numbers_msg emit-on-toggle/suppress-when-unchanged; protocol
version pins updated to 13. Validated: fmt + clippy --all-targets clean
both flavors; 1440 lib + 12 protocol + 53 pmacs-gpu tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Mirror the TUI line-number gutter in the pmacs-gpu frontend — the GPU half
of sub-arc 1. Frontend-local, no protocol change (Q#UX1); off by default,
enabled with `--line-numbers`.
The gutter is a reserved left strip mirroring the minimap's reserved right
column. All horizontal text geometry hangs off `TEXT_LEFT`; the gutter adds
`gutter_width_px()` to it via `text_left()`, applied at every byte→pixel x
site (main TextArea, caret, washes/squiggles) and subtracted at the one
pixel→byte site (mouse hit-test). The main text clip-left moves off 0.
- gutter_width_px = digits(line_count) * mono_advance + gap (px), advance
read from the shaped code buffer.
- A dedicated gutter_text_renderer + gutter_buffer draw right-aligned dim
numbers, reshaped per scroll (refresh_gutter_buffer), same font size +
line height as the code so rows align one-for-one.
- --line-numbers flag filtered out before the mode parser (position-
independent), threaded App → State.
Headless render test asserts enabling the gutter changes the frame (ink +
shift). fmt + clippy clean; 53 pmacs-gpu tests pass (render tests on the
local adapter). Needs a human eyeball before the PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
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
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
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
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