`tests/resource_reconciliation_acceptance.rs`, 23 rows, no dired
content — items 23–37 and 50–55 driven through the real entry points:
`pmacs.fs.rename` / `pmacs.fs.remove` fire-and-forget for the drain
harvest, `pmacs.buffer.apply_resource_op` for the synchronous arm, and
the fake server's `workspace/applyEdit` for the applier.
The rows that took design rather than transcription:
Item 27 opens two descendants AND two buffers on one exact path, since
one child would not defeat a first-match lookup. Item 29 tests name
provenance in both directions, including a name explicitly set to a
string that normalizes to the file's own path — the case a
path-equivalence heuristic gets wrong. Item 30 paints a real frame and
counts diagnostic underlines per window rect, because
`DiagnosticView.uri` is private and a store assertion would prove
nothing about re-rooting; it also pins each overlay's index in the
composition order, which is what a remove-and-re-push breaks. Item 53b
states its three assertions individually, since a compound check can
pass on two of the three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`src/lsp.rs` gains nine: the fourteen-family store inventory with a
17-entry precondition so it cannot pass vacuously, the route purge with
`workspace/symbol` and another server's route both surviving, the
awaiter drain joined on the rid, the error contract's two arms, the
late-publish drop with its does-not-over-reach companion, the
`mark_document_stale` gate across all three stale stores, exact-pair
tombstone identity, and reclamation under both `start_generation` and
terminal `forget`.
`src/diag.rs` pins that `forget` drops the epoch while `clear`
deliberately bumps it — the leak a `clear`-based forget would leave in
the one map nothing prunes.
`src/async_runtime.rs` injects two resource replies onto the private bus
in each order and asserts `TickOutcome.resources` reports arrival order,
not allocation order; plus that a failed or cancelled mutation is not
harvested at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One shared walk query (`buffers_bound_under`), lifted out of #190's
`delete_verdict` so the guard and both reconciliation seams cannot
disagree about which buffers an operation touches: every buffer, both
sides normalized, component-aware containment.
`EditorCore::reconcile_rename` moves the stored path and — only for a
`PathDerived` name — the buffer name. `EditorCore::reconcile_delete`
composes the same two removal phases `pmacs.buffer.kill` composes,
preflighting `editing_in_progress` because a `ConcurrentEdit` refusal
arrives after `kill_buffer` has already moved windows. Phase 2 stays
with the caller; `EditorCore` gains no Lua handle.
`AsyncRuntime::tick` now returns a `TickOutcome` carrying the settled
ids plus the successful resource mutations, in bus-arrival order, which
is documented as not being execution order. `PendingJob.resource`
retains the paths the dispatchers move into the worker closure.
`LspManager::forget_uri` purges the routes carrying a URI, drains the
awaiters joined to them on the rid, and clears all fourteen stores plus
`documents`. A generation-scoped exact-pair tombstone gates the two
uncorrelated writers that can otherwise resurrect what it cleared:
`publishDiagnostics` and `mark_document_stale`, which now takes a
server id. `ResponseRoute::scoped_uri` is the one variant list, with
`uri()` delegating to it.
New Lua surface: `pmacs.buffer.set_name`, `pmacs.lsp.forget_uri`,
`pmacs.diag._rename_resource`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`BufferNameOrigin` records where a buffer's name came from instead of
inferring it from the string: a path-backed buffer's name is the path
*as given*, so a relative open is named `foo.rs` while its stored path
is absolute, and a user may legitimately choose a name that normalizes
to its own file's path. Rename reconciliation asks the bit.
Every path-backed creation site is audited onto the new
`set_path_derived_name` door: `EditorCore::get_or_load_buffer`, the
`NotFound` arm of `resolve_target_buffer`, `pmacs.buffer.from_file`,
and `pmacs.buffer.find_or_open`. Ordinary `Buffer::set_name` records
`Explicit`.
`View::rename_resource` is the seam that re-roots a URI-keyed overlay
in place, so it keeps its position in the window's composition order;
`DiagnosticView` overrides it, whose `uri` is private and set once at
construction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Implements docs/folding-stage2-framing.md rev 4. The daemon grid
renderer now consults the fold store: hidden lines are omitted, rows
below shift up, and every consumer that assumed
`display_row = source_line - view_top` routes through one shared
projection. No wire schema change and no protocol bump (Bet B6) — the
collapse is entirely daemon-side; the GPU path is Stage 3.
The spine (Q#FD12) is `src/fold_view.rs`: a `VisibleLineMap` derived
from `FoldRegistry::folds` plus a window's line offsets and never
stored. Its unit is a merged **hidden component** — overlapping OR
adjacent hidden intervals unioned, each keeping the one visible
`head_line` and that line's exact `head_position`. Adjacent intervals
merge because the later fold's head is itself hidden, which is what
makes nesting, shared heads, and crossing overlap all resolve to a
head that can actually render (round-3 F2).
Instances are short-lived and built **per rendered window** and **per
command/event operation**, never once per frame: `paint_frame` renders
several windows that may show different buffers, so a singleton would
leak one pane's folds into another (round-2 F2). The render instance
rides on a lifetime-bearing `Viewport<'a>` as `Option<&'a
VisibleLineMap>` — a shared ref is `Copy`, so `Viewport` stays `Copy`
(Bet B7).
Rendering:
- `TextView::render` walks visible lines; the head line gets a
trailing content-area ellipsis (Q#FD13).
- The gutter walks visible lines too: Absolute keeps the raw `line+1`,
Relative/Hybrid measure VISIBLE distance anchored on the cursor's
visible head (Q#FD14). The fold glyph takes the col-0 sign cell only
when a gutter exists — line numbers default to Off, so with no gutter
the ellipsis is the sole marker (Q#FD20, round-1 F3). A diagnostic
clamped onto the head wins that cell by paint order.
- A diagnostic on a hidden line clamps its SIGN to the outermost
visible head (most-severe merge); the squiggle needs a real row, so
only the sign clamps (Q#FD15).
- Caret, local selection endpoints, and peer cursors project via
`visible_position_of` — the head row AND the head's end-of-content
column, never an arbitrary column (round-2 F3). Peer presence derives
the RECIPIENT window's map.
- Style/search/completion overlays route through
`Viewport::row_offset_of`; the mode-line indicator reckons in
visible-line space.
Command/event time is scoped per frontend (Q#FD21): a
`fold_projection` flag on `FrontendView`, set at attach from the
negotiated `semantic_render` bit (grid ⇒ true, semantic ⇒ false until
Stage 3, LOCAL ⇒ true) and never inferred from a `FrontendId` (Bet
B8). Without it, shared `EditorCore` motion would make a simultaneous
unfolded GPU session's cursor skip lines it still displays. The map's
two axes stay separate (round-3 F1): the acting frontend supplies the
policy, the operation's TARGET window supplies the buffer — a wheel
event names a pane without activating it.
Motion (Q#FD17, ruled: include), paging, wheel, the click inverse, and
the auto-scroll clamp all step by visible lines under that gate;
motion from a hidden logical cursor normalizes to the visible head
first. `view_top` stays a source-line index (Bet B5), set only via
`clamp_view_top` so it never rests hidden.
Unfold widening (Q#FD19): the pre-edit unfold moves to the top of
`apply_active_edit` — one funnel that subsumes the six primitives'
calls and covers yank + query-replace, both of which place point at
the edit site first. Interactive Lua mutators hook the common
`run_buffer_edit`, above the managed/bypass split, gated on
`InteractiveCommandOrigin` AND the edit targeting that frontend's
active-window buffer. The remote/optimistic-CRDT path stays excluded
(Stage 3); undo/redo unfold stays deferred.
Acceptance: `tests/folding_stage2_acceptance.rs`, 35 tests asserting
on the real `paint_frame` cell grid, covering framing items 1–14
including crossing folds, a nested deeply-hidden cursor, a split of
two different buffers with an inactive-pane wheel, and simultaneous
grid+semantic motion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
Share one fixed eight-column tab-stop contract across core and GPU renderers. Consolidate byte-to-display-column accounting, expand GPU code tabs with source provenance, align caret/hit/decoration geometry, and refresh minimap projection on edits.
The round-4 freeze read counted the retained for_uri vector inside
status_facts_msg -- correct, but StatusFacts runs at frame cadence
for every semantic session under the shared store mutex, so a long
stale interval cost O(frames x diagnostics x sessions).
DiagnosticStore now maintains per-URI severity totals alongside
by_uri: set replaces them (an empty publication removes them with
the vector), clear removes them, and mark_stale deliberately
preserves both -- entries exist exactly when by_uri entries do, and
set/clear are the store's only by_uri mutators. status_facts_msg
reads the tuple in O(1), and the all-URI severity_totals sum reuses
the cached tuples.
Store unit (acceptance item 34) pins the invariant: all four totals
replace correctly, survive staleness, and clear with the diagnostic
vector; empty_set_clears_uri asserts the totals drop too. The
rounds 3-4 freeze acceptance passes unchanged -- behavior parity,
so the unit pin is the evidence (no runtime bite exists for a
behavior-preserving refactor). Round-5 implementation
user-authored; this commit folds it with framing revision 9, the
protocol doc's O(1) note, and the acceptance manifest pointer to
the item-34 unit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
Arc 4 stage 1 (docs/theme-faces-framing.md, revision 4). Faces are
theme entries under the reserved ui/ui.* namespace -- zero new Lua
API. Theme::face() resolves with the dotted-prefix walk but never
falls back to default_style; each face applies owns-surface within
its stage-1 component mask, identical on both frontends.
Substrate: two monotonic theme mutation counters (syntax/face) with
transactional set/merge/clear/default (parse before locking, commit
all-or-nothing, bump from the prior value); the StyleGate and the
minimap summary key on the counters -- fixing the pre-existing bug
where a mid-session pmacs.theme.set never re-shipped StyleSpans --
with the summary gaining payload-equality suppression that still
advances its key on computation.
Wire: InstanceMessage::ThemeFacts appended after CompletionPopup
(postcard discriminants are ordinal; a byte pin guards placement),
PROTOCOL_VERSION 15 -> 16, daemon-gated >= 16, one authoritative
table per attachment (None-seeded baselines), TUI silent-drop arm.
Grid: paint_frame resolves ui.modeline / ui.statusline /
ui.minibuffer(.candidate) / ui.gutter / ui.selection faces;
SearchView and DiagnosticView take the theme handle through the real
attachment paths (EditorCore injection, install_diag threading); the
canonical severity color resolves ui.diag.* with the Default ->
built-in policy that keeps the minimap presence encoding sound.
GPU: exact-name face table applied per draw with the Q#TH5 Default
mapping (plain text / window bg, reverse swap), local/peer wash
split, candidate-dropdown glyph site, and the status-band
shaping-cache invalidation without which a diag-face recolor with
constant counts kept stale counter colors.
Tests: 18-test acceptance suite (grid, wire, daemon gate, atomicity,
monotonicity, late join), 7 GPU headless tests incl. decoded vertex
colors, units for the face walk / transactional commits / producer
caches; protocol pins for v16 + the CompletionPopup byte pin.
Bites vs 3cbb9de (scripts/bite): semantic_render.rs (8 runtime test
failures), editor.rs (5 runtime), daemon.rs (v15 gate, runtime);
lua_bindings/mod.rs, pmacs-gpu/main.rs, search.rs, diag.rs, and
highlight.rs bite as compile failures (weaker evidence, disclosed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
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
The search store now hangs off EditorCore (reachable by the
producer, the Lua commands, and the TUI view). The decorations
producer emits SearchMatch for every visible match and
SearchMatchActive for the active one, byte-range-direct (no
line/col conversion), viewport-clipped, and stale-skipped on the
M11.8 model. pmacs-gpu wires the two decoration_kind_to_bg_color
arms (translucent yellow / stronger amber). The TUI gets a
SearchView overlay mirroring DiagnosticView (black-on-yellow wash,
brighter for the active match), reusing diag.rs's now-pub(crate)
line/col helpers.
Nothing populates the store yet (commit 3 wires the input), so the
paths are dormant until then — verified by populating the store
directly: producer emits the right kinds + stale-suppresses, the
TUI view washes the cells.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GPU's gutter-sign equivalent (framing Q#D3). The producer folds
diagnostics into the per-line summary: each touched line's dominant
style gets the severity's canonical underline_color (most severe
wins), skipped while the URI's store entry is stale — same
discipline as the decorations producer. The minimap stroke prefers
underline_color over the syntax fg, so error/warning lines read at
a glance.
Diagnostics publish without a CRDT generation bump, so the
summary's generation-keyed cache gains a second key: a new per-URI
epoch on DiagnosticStore (bumped on set/clear, not mark_stale). A
republish re-emits the summary; everything else stays suppressed
(framing bet #3 — the gate widens precisely, not naively).
DiagnosticSeverity::underline_color() becomes the canonical palette
(TUI squiggles, col-0 markers, and minimap marks all share it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: a removed comma produced the column-0 marker but no
squiggle. rust-analyzer anchors 'expected COMMA' as a zero-width
range one past the line's last character (verified:
`rust-analyzer diagnostics` reports col 12 → col 12 on a 12-byte
line), and the per-line clamp collapsed it into the empty-range
skip. Zero-width ranges now underline the single cell at the
anchor — one past EOL is a blank cell inside the window, and a
squiggled space is how other editors surface exactly this error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
M4.6 follow-up piece 3. The TUI reserves no gutter column, so the
sign is a severity-colored background on the line's first cell:
the glyph and its syntax color survive (DiagnosticView's contract
stays style-only), most severe diagnostic per line wins, and
zero-width ranges — which the underline pass cannot paint — now
have a visible artifact, closing a long-stale comment's promise.
Co-Authored-By: Claude Fable 5 <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>
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>
Surfaced during session-5 manual validation as the final iteration
of bet #1 from the framing pass: edits that shift byte positions
left old diagnostic colors painted over post-edit text in both
`pmacs-gpu` and (now-visible) the TUI. Persisted for the full LSP
re-analysis window (100ms–5s).
PR #47 fixed the StyleSpans side via generation-tracked full=true
emission, but Decorations remained vulnerable: the producer's diff
shipped old diagnostics from the diag store, whose entries were
indexed at pre-edit byte positions until clangd republished.
Fix: a per-URI `stale_uris` flag in `DiagnosticStore`. The LSP
layer's `did_change_full` marks the URI stale right after sending
the notification; the next `publishDiagnostics` absorb path's
`set` clears it. The `semantic_render` producer reads `is_stale`
and skips diagnostic emission entirely while stale.
Effect: between an edit and clangd's next publish, the producer
ships zero diagnostic decorations. Frontend's replace/merge clears
old positions cleanly. Brief uncolored window (≤ LSP re-analysis
latency) replaces the previous wrong-position-color persistence.
The correct visual tradeoff: honest emptiness over deceptive
staleness.
Files changed:
- `src/diag.rs` — `DiagnosticStore` gains `stale_uris: HashSet<String>`;
new `mark_stale` / `is_stale` API; `set` and `clear` reset the
flag on the assumption that absorption / explicit removal mean
the LSP has caught up.
- `src/lsp.rs` — `LspManager::did_change_full` calls
`diag_store.lock().mark_stale(uri)` after `send_notification`.
- `src/semantic_render.rs` — `scoped_decorations` reads `is_stale`
alongside `for_uri`; when stale, suppresses the diagnostic
loop (selection and other non-diagnostic kinds still emit).
Tests (all crdt-gated where they reference semantic_render):
- `diag::tests::stale_flag_default_false`
- `diag::tests::mark_stale_sets_flag` (per-URI scoping)
- `diag::tests::set_clears_stale_flag`
- `diag::tests::empty_set_clears_stale_flag_too`
- `diag::tests::clear_drops_stale_flag`
- `semantic_render::tests::diagnostics_suppressed_while_diag_store_stale`
— assert no diagnostic kinds emit while stale; assert they
re-emit after a fresh `set` clears the flag.
Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1482 (+6) with crdt; 1319 (+6) without; m4 83; m11_5 2.
This is approach (A) from the session-5 ask: track per-URI freshness
relative to buffer edits, suppress emission until LSP catches up.
Approach (B) — clangd's didChange/publishDiagnostics version
matching — would be more precise but requires plumbing version
tracking through the LspManager's document state, which is a
larger change deferred. The stale-flag captures the same semantic
("any edit since last publish ⇒ stale") at a cheaper cost.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>