Commit Graph

4 Commits

Author SHA1 Message Date
Levi Neuwirth e1b859fd4f feat(generated-buffers): dired and listview adopt the authorized write
Stage 1 of generated-buffer immutability
(docs/generated-buffer-immutability-framing.md, revision 5). Closes the
two families the bug is reachable on WITHOUT `M-x`: `compile.lua` and the
search panel rebind all seven undo chords to a no-op, but `dired.lua` and
`listview.lua` rebind nothing, so a bare `C-/` emptied a listing and a
panel. The cheap half is also the exposed half.

An intercept is not read-only. `Buffer::undo` reaches the rope through
`ensure_writable` and never consults the intercept chain, so the
erroring-intercept-plus-`bypass_intercept`-over-a-writable-rope idiom
guarded the edit path and left the history path open. Rebinding chords
does not close it: `M-x buffer.undo` is dispatchable on every buffer in
the tree.

- `dired.lua`'s `paint` and `listview.lua`'s `render` write through
  `pmacs.buffer.set_generated_contents` — lift the lock, whole-buffer
  replace skipping intercepts, discard history, re-assert the lock, fan
  the `Edit` out. Zero `bypass_intercept` writes remain in either file.
- Both keep their named erroring intercept and `set_round_trip_input`.
  The layering at `terminal.lua:351-366` is unchanged: the rope lock
  protects the daemon copy, round-trip input protects a semantic
  frontend's own mirror, and neither substitutes for the other.
- Q#GB13 — `listview.ensure_panel` stops adopting a same-named foreign
  buffer. Ownership is the `panels` table; a collision disambiguates
  `<2>`..`<99>` and raises at the limit, matching `dired.lua:476-504`.
  This is a prerequisite of the lock, not a follow-up: the arc removes
  the `M-x buffer.undo` that was the only recovery from a clobber.
- Q#GB18 — `panels` becomes a compacting list keyed by identity. It was
  written under the requested name and read back under the actual name,
  which a disambiguated panel breaks: `RET`, `g` and `q` fail closed and
  silently, and `listview.open`'s capture guard fails OPEN, capturing a
  panel as its own `q` target — the chained-panel loop its comment says
  it prevents. Ships in the same commit as the disambiguation by the
  framing's ordering constraint.
- Q#GB6 — `EditorCore::notify_buffer_edit` clamps each window coordinate
  against its own post-edit bound, unconditionally. `cursor` is a byte
  position bounded by `Buffer::len`; `view_top` is a line index bounded
  by `TextView::line_count`, and a replace can grow in bytes while
  collapsing lines, so "the buffer shrank" is not a usable trigger. This
  fixes a shipped defect that reaches terminal copy mode.
- Q#GB16(a) — locking these families disables fold CREATION on them,
  because `document_bytes` is spelled `is_read_only()`. Accepted and
  stated rather than shipped silently; the status string now names the
  read-only lock instead of claiming "not a document buffer".

Acceptance: 10 new criteria in `listview_acceptance` (16 total), 6 in
`dired_acceptance` (31 total), 2 in `terminal_copy_mode_acceptance`.
Every criterion's falsifying mutation was run: 5 bite by revert against
`githubsucks/main`, 9 by a named one-line mutation.

Two framing corrections, both recorded in the tests rather than worked
around silently:

- Stage 1 criterion 5 is unreachable as written. `Buffer::apply_edit`
  (`src/buffer.rs:773`) and `begin_edit` (`:725`) call `ensure_writable`
  as their FIRST statement while the intercept chain runs later inside
  `apply_edit_inner` (`:1072`), so once this arc's lock is installed an
  ordinary edit can never reach the intercept. Restated at the one point
  where the two are distinguishable — the lock lifted — which is the
  state the intercept genuinely still covers.
- Criterion 7 cannot bite at the listview adopter. `listview.refresh`
  and `listview.open` both follow `render` with `window.switch_buffer`,
  which rebuilds the `TextView` from scratch and masks a dropped
  fan-out. `dired.revert` does not, so the dired half carries the bite;
  it fails under the mutation with the reported
  `assertion failed: end <= self.len()`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 09:50:09 -04:00
Levi Neuwirth 90fc7a913e fix(window): wire the side-window split guard and scope the divider drag
PR #155 review round 1.

Finding 1 (must fix): `try_split_active` had no production caller —
`pmacs.window.split_horizontal` / `split_vertical`, and therefore
`C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a
focused panel made the root wrapper's final child a split rather than
`Leaf(side)`, which both `Layout::compute`'s fixed pass and
`document_subtree` key on: the panel band reverted to 1:1 weight
division and an ordinary window ended up living inside it. Both bindings
now route through the guard, and acc26 asserts through the real Lua
path — a direct core call passes with the guard unwired, which is how it
survived the first round.

Finding 2: the armed-drag early return now checks the arming frontend,
so one frontend's in-flight gesture cannot cancel or swallow another's
mouse events. New acc30c.

Finding 3: `paint_mode_line_graphemes`'s doc block was left heading
`paint_divider_segment`; moved back.

Finding 4: a recompile carries no `display`, so it took the raw switch
and duplicated a panel-placed `*compilation*` into the document window.
`start_run` now detects that the buffer already owns the panel slot.
`pmacs.window.buffer` gained an optional window argument so an adopter
can ask without selecting the panel first. New acc19b.

Stage-2 hazard pins the review asked for, both in `src/daemon.rs`:
a fresh attach while LOCAL is focused in a panel inherits LOCAL's
document buffer, and an initial-target bootstrap whose `after-load`
hook creates and selects a panel still reasserts into a document window.

Minor: dropped listview's dead `p.side`; documented `focus_window`'s
caller-validates contract; `jump_back` restores through `focus_window`
so the "every focus change" contract holds; `params` / `resize` default
to the acting frontend's selected window rather than the ambient one;
widened the flexible-division math to u64 intermediates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 15:07:35 -04:00
Levi Neuwirth 683c9b86aa feat(window): adopter placement opt-in and the Stage 1 acceptance suite
- `listview.open`, `compile.run`, and `pmacs.terminal.open` all take the
  same strict `display = "current" | "panel"`, validated before any
  buffer, session, process, or wrapper exists. Omission keeps today's
  behavior; Stage 3 flips the default.
- `listview.quit` / `compile.quit` delegate to `window.quit` only when
  the buffer really is in a side window, so the presentation is deleted
  or restored instead of leaving a source buffer stranded in the slot.
- LSP `visit_location`, LSP go-to-definition, and compile `visit_error`
  route through `display_file`, so a visit from a panel lands in the
  document target and fires its hook with that window active.
- `window.quit`'s Delete arm focuses the revalidated remembered origin.
- Capability fallback discards an accompanying `height` rather than
  rejecting the call.
- `window.min-height` clamps a below-floor value on read instead of
  refusing the write.
- `tests/bottom_panel_stage1_acceptance.rs`: 42 tests over the framing's
  Stage 1 criteria, including the two production `Layout::compute`
  callers, the recursive minima, hide/reappear, the final-focus matrix,
  quit chains at the depth cap, per-frontend jump origins, the divider,
  and a real-PTY pin of Bet B1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:10:45 -04:00
Levi Neuwirth b25b47334d feat(panels): listview module, Q#P6 round-trip seam, references panel
Arc 1b phase 1 (framing: docs/lsp-panels-framing.md).

Q#P6 (the one Rust change): EditorCore.round_trip_buffers +
pmacs.buffer.set_round_trip_input(buf, on); dispatch_idle() reports
false while a marked buffer is active, so semantic frontends'
optimistic-apply stays off -- RET reaches a panel's buffer-local visit
binding instead of locally inserting a newline, and typing dispatches
into the edit path where the read-only intercept rejects it (a CRDT
import would bypass the intercept chain entirely). Pruned on kill.

Q#P1/P2/P3: builtin/runtime/listview.lua generalizes the *buffer-list*
idiom -- pmacs.listview.open{name, header, rows, on_visit, on_refresh}
owns ensure-buffer (recreates if user-killed), wholesale render with
bypass_intercept, line->item map, buffer-local RET/SPC/n/p/g/q keymap,
previous-buffer capture + q restore (never another panel; scratch
fallback), cursor re-seat after render, the read-only intercept, and
the Q#P6 mark. Panels are buffers: both frontends render them with
zero protocol change.

Q#P4: lsp.find-references (M-?) opens *references* -- one row per
location, paths shortened against the project root, RET visits via the
shared SP-4 template (jump ring, find_or_open, cursor walk; extracted
as visit_location for the phase-2 outline to reuse).

Acceptance: tests/listview_acceptance.rs -- open/seat/visit, header
non-visitable, q restore, read-only rejection, dispatch_idle gate,
refresh re-render + re-seat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:15:46 -04:00