Merge pull request #247 from levineuwirth/gui-stage1b-pointer-scroll
GUI arc Stage 1b — pointer and scroll
This commit is contained in:
commit
27743fd8a0
|
|
@ -2627,6 +2627,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"glyphon",
|
||||
"loro",
|
||||
"pmacs",
|
||||
"pmacs-protocol",
|
||||
"pollster",
|
||||
"sys-locale",
|
||||
|
|
|
|||
|
|
@ -301,6 +301,471 @@ waits for a signal that is not coming.
|
|||
- **THE FIRST DISPATCH IMMEDIATELY FOUND A RED ON `main`**, which is
|
||||
what this lane was built for. See the proptest entry below.
|
||||
|
||||
## GUI arc Stage 1b — pointer and scroll — IN REVIEW (PR #247)
|
||||
|
||||
**PR #247** — `https://github.com/levineuwirth/pmacs/pull/247`, opened
|
||||
2026-09-02 against `main` at `0ec13b3`, 13 files, on the gate-green
|
||||
head. **Not to be merged unprompted.**
|
||||
|
||||
**CI is GREEN at `5fcc3a3`: 14 of 14 checks, zero non-success**, and the
|
||||
PR reports `MERGEABLE` / `CLEAN`. The two macOS legs are the long poles
|
||||
(13m47s and 21m12s); `Test (crdt)` — the flavor `scripts/gate`'s clippy
|
||||
step cannot lint locally — passed at 11m21s. Checks ran against the
|
||||
current head, not an earlier one.
|
||||
|
||||
|
||||
**Branch `gui-stage1b-pointer-scroll`, rebased onto `0ec13b3`.** The
|
||||
**remote ref `githubsucks/gui-stage1b-pointer-scroll` is
|
||||
authoritative**; recover with
|
||||
`git fetch githubsucks && git checkout -B gui-stage1b-pointer-scroll githubsucks/gui-stage1b-pointer-scroll`.
|
||||
|
||||
**The rebase rewrote history**, so that ref was force-pushed. The seven
|
||||
pre-rebase framing commits are preserved on
|
||||
**`githubsucks/gui-stage1b-prerebase-backup`** (`5c83b15`) — pushed
|
||||
before the rewrite, not after, so nothing depended on a local-only ref
|
||||
surviving.
|
||||
|
||||
Framing
|
||||
`docs/gui-stage1-input-framing.md`, **revision 22** — §2a re-measured at
|
||||
this base, the panel-replay prerequisite recorded as DISCHARGED by #243,
|
||||
and (revision 21) the GPU's authority recorded as **structural**, its
|
||||
lifetime tables made frontend-specific. **The both-axis effect witness
|
||||
is no longer owed**; see the completion note below, which is the
|
||||
authority on what this lane still has outstanding.
|
||||
|
||||
**Landed so far** (latest verified code head `ef25920`; **full review
|
||||
gate green at that exact head**, run `20260902T180422Z-311523`, all 16
|
||||
steps)**:** B1's per-target fractional
|
||||
wheel residual (the producer), B2's daemon-side horizontal panel leg,
|
||||
B3/B7's shared `scroll_window_columns` with its saturated bound and wrap
|
||||
pin, B4's middle-click PRIMARY paste, **B6's minimap wheel routing**,
|
||||
and **B5's I-beam**, whose icon is
|
||||
re-derived in **`reshape`'s tail**. Panel inset transitions now terminate
|
||||
at `reshape_if_panel_band_changed`: accepted `Present`/`Absent`/row-count
|
||||
changes and a geometry declaration disowning the retained frame all
|
||||
reach the hook, while a content-only frame repaints without rebuilding
|
||||
the document. Three production appliers remain, one per cause (pointer
|
||||
motion, menu ownership without geometry, geometry).
|
||||
|
||||
B5's central-hook correction needed one more pass. Counting ten
|
||||
`reshape` call sites did **not** establish that every boundary-changing
|
||||
transition reached one: `PanelFrame` synchronized dimensions and
|
||||
redrew without reshaping, and advancing `geometry_epoch` disowned the
|
||||
retained frame *after* resize/font handling had reshaped against its old
|
||||
presentation. The earlier two path patches — the snapshot arm, then a
|
||||
`set_menu` helper — had the dual defect: each masked the other, so a
|
||||
row's documented mutation named something the row could not see. **A
|
||||
witness whose mutation is masked is not weaker evidence than a real
|
||||
one; it is evidence of nothing, and it reads identically in the diff.**
|
||||
|
||||
At `175cc7b` — B5's own checkpoint, not the lane's current one — all
|
||||
309 GPU rows pass; workspace clippy with all targets and `-D warnings`,
|
||||
fmt and `git diff --check` are clean. Four mutations were executed and
|
||||
each fires its named row: omit accepted-frame reflow, omit
|
||||
epoch-invalidation reflow, reshape content-only frames, and retain
|
||||
divider hover on `Absent`. **B6's verified checkpoint is `f441d3d` at
|
||||
312 GPU rows**; the latest verified code head is named once, at the top
|
||||
of this block. The authoritative remote ref above may be ahead with
|
||||
documentation-only commits and remains the recovery source. Calling a
|
||||
fixed checkpoint the lane's "current head" here would become false in
|
||||
the next code commit — the self-certifying checkpoint defect this
|
||||
ledger has already recorded elsewhere.
|
||||
|
||||
### The latch was never the thing the ledger said it was
|
||||
|
||||
**This entry was wrong, and the correction is the recovery-relevant
|
||||
part.** It listed the manual horizontal authority latch as "landed but
|
||||
not yet witnessed". Writing L1 showed otherwise:
|
||||
`manual_left_authority` existed on the GPU, was **written in four
|
||||
places and read in none**, and did not exist on the TUI at all. What
|
||||
had landed was the arming. The preservation the latch exists for was
|
||||
nowhere, on either frontend.
|
||||
|
||||
L1 measured it before anything changed: a sideways wheel moved the TUI
|
||||
origin to 30, and the next paint put it back to 0. **A horizontal
|
||||
scroll was undone by the following frame.** An unread `bool` preserves
|
||||
nothing, and "landed but unwitnessed" is exactly how that reads from
|
||||
the outside.
|
||||
|
||||
**`9cb610e` implements clauses 2–5 for the TUI** and witnesses each:
|
||||
L1 preservation across a real `paint_frame`, L3 release on a genuine
|
||||
cursor move, L4 a vertical wheel preserving authority, L5 viewport-only,
|
||||
L6 an absorbed notch arming nothing, L7a re-clamp on widening, L8 wrap
|
||||
clearing the latch. Seven mutations run, each biting its named rows.
|
||||
|
||||
Three things the framing's L-table did not anticipate, all surfaced by
|
||||
setup assertions rather than by reading:
|
||||
|
||||
- **L4's stated hazard cannot arise in the setup the same table
|
||||
mandates.** The vertical wheel carries point only when the caret is
|
||||
INSIDE the viewport — `pos_to_display` returns `None` left of the
|
||||
edge (Q#HS7(c′)) — and every other L-row requires it outside. L4
|
||||
places the caret inside and uses the **latch** as its discriminator,
|
||||
because the origin cannot discriminate there.
|
||||
- **Clause 3 needs a mechanism, not an exemption.** `scroll_window`
|
||||
refreshes `manual_left_cursor` to the point it dragged along, so
|
||||
clause 4's "genuine cursor change" stays false. Keying release on the
|
||||
cursor byte alone releases exactly where clause 3 forbids it.
|
||||
- **`paint_frame` runs the follow BEFORE resolving the frame's wrap
|
||||
mode and content width**, so a geometry or wrap change reaches the
|
||||
follow one frame late. L7a and L8 paint twice and say why.
|
||||
|
||||
**Clause 5's replacement half took two passes, and the first census
|
||||
was wrong.** `a2d5b26` named "three TUI paths" — the ones that had been
|
||||
thought of. A grep for every write of a window's `buffer_id` finds
|
||||
**four** production sites: `switch_active_buffer_for`,
|
||||
`install_buffer_in_window`, **`kill_buffer`'s fallback rebind**, and
|
||||
the daemon's `align_primary_document_window`. None cleared `view_left`
|
||||
or the latch. `EditorCore::from_bytes` also assigns one and is
|
||||
deliberately excluded — it builds a fresh core with no prior origin to
|
||||
inherit — recorded so the next census need not re-decide it.
|
||||
|
||||
**A census taken by recall is not a census.** This is the second time
|
||||
in this lane a count was stated from memory and found short by review;
|
||||
the first was `reshape` call sites that no geometry path reached. Three
|
||||
of the four already reset cursor, selection and `view_top` a line at a
|
||||
time; the horizontal origin was simply missing from the list. One
|
||||
`Window::forget_manual_horizontal_origin`, four call sites, four rows
|
||||
(L8b–L8e), each firing only on its own call site's removal. L8d sits in
|
||||
`daemon.rs` because that function is private there.
|
||||
|
||||
Two witness repairs in the same commit, both of the same shape — an
|
||||
assertion that looked strict and was not:
|
||||
|
||||
- **L7a asserted only that the origin came DOWN.** Any arbitrary
|
||||
reduction satisfied that, including an off-by-one that strands a
|
||||
column. It now asserts `widest − viewport` exactly, against a named
|
||||
fixture constant, and is mutation-checked with that off-by-one.
|
||||
- **L4's stated rationale was false.** It claimed the caret stays
|
||||
inside the viewport after the vertical wheel; with short filler lines
|
||||
the caret clamped to their end, LEFT of the origin — so the origin
|
||||
discriminated too and the reason given for using the latch instead
|
||||
did not hold. The filler is now 120 columns and the row asserts the
|
||||
caret is still inside, through `pos_to_display` — **on both edges.**
|
||||
`is_some()` alone rules out only the left one; a caret past the RIGHT
|
||||
edge still returns `Some`, and there a normal follow moves the
|
||||
origin, which is exactly the state the row claims cannot
|
||||
discriminate. The probe requires `col < last_content_cols`.
|
||||
|
||||
**L7b landed at `feda851`** — clause 3's content half, the shrink that
|
||||
lowers `widest − viewport` with the viewport untouched, asserted on the
|
||||
exact new bound and firing on both the release mutation and an
|
||||
off-by-one.
|
||||
|
||||
### The GPU's latch is unreachable, and its real gap was the clamp
|
||||
|
||||
`ec6444e`. **The framing's L2 could not witness the latch it was offered
|
||||
as evidence for.** It proposes wheel-sideways then a height-only resize
|
||||
as the GPU's manual-authority row. Measured before anything was added:
|
||||
the origin
|
||||
survives that resize with `manual_left_authority` **never read anywhere
|
||||
in the frontend**. Q#F6's painted-before policy is what preserves it —
|
||||
`resize` runs `ensure_caret_painted` only when the caret was painted,
|
||||
and a caret scrolled off screen is not painted, so the follow that
|
||||
would snap the origin back never runs. When the caret IS painted it is
|
||||
inside the viewport, where `follow_left` returns the origin it was
|
||||
given. **The latch is unreachable on this frontend by either branch.**
|
||||
|
||||
What the GPU actually lacked was clause 3's clamp, for the same reason:
|
||||
nothing brought the origin DOWN when the maximum fell, because the only
|
||||
code that would is the follow that is skipped in that state.
|
||||
**Measured: scroll to the right bound at 640px, widen to 1600px, and
|
||||
the origin stayed 960px past the new maximum** — most of the viewport
|
||||
blank, the text off its left edge. `clamp_code_scroll_left` now sits at
|
||||
`reshape`'s tail, beside B5's icon hook and for the same reason.
|
||||
|
||||
**This was owed to the framing, not to the code: L2's wording promised a
|
||||
latch witness this frontend cannot provide. Revision 21 discharges it**
|
||||
by naming the painted-before policy as the mechanism and splitting the
|
||||
GPU rows by the behavior each actually witnesses.
|
||||
|
||||
**Settled (user decision, 2026-09-02): the GPU latch is deleted, not
|
||||
completed.** The contract is behavioral, and the two frontends are not
|
||||
required to share a representation; a reader would have duplicated the
|
||||
painted-before policy and needed a cursor baseline of its own to avoid
|
||||
suppressing genuine cursor movement. `manual_left_authority`, its
|
||||
initializer and its four writes are gone, and `scroll_by_columns`
|
||||
no longer returns an unused `bool`. **GPU authority is structural**,
|
||||
and the framing now says so.
|
||||
|
||||
The GPU's four rows: **L2** (height-only preserves — the policy, not a
|
||||
latch), **L7a** (widening clamps to the exact bound), **L7b** (a
|
||||
content shrink clamps **through the incremental edit path**), and
|
||||
**L3** (a moved `CursorByte` pulls the viewport back). Each fires on
|
||||
its own mutation; L2's also necessarily bites L7a, which its doc names.
|
||||
|
||||
**L7b, GPU, is the one that found a live gap.** Q#R1's keystroke case
|
||||
re-shapes only the affected line through `try_reshape_line` and skips
|
||||
the full `reshape` — **and skipped clause 3's clamp with it**. A
|
||||
one-line delete shortening the widest line could leave the viewport
|
||||
past the end of the text with no later event to repair it. The clamp
|
||||
now runs on that branch too. The row keeps a redraw-deadline sentinel
|
||||
that full `reshape` clears, so it positively proves the incremental
|
||||
branch ran; unchanged line count alone would only make that branch
|
||||
eligible, not establish that `try_reshape_line` succeeded.
|
||||
|
||||
**Nothing is landed-but-unwitnessed on this lane any more.** R4 and R5
|
||||
now have their replacement rows (`e5ab16c`), one clear omitted at a
|
||||
time so neither field hides behind the other's reset; the wrap and
|
||||
buffer-replacement clearing that stood here is covered by L8 and
|
||||
L8b–L8e, and that bullet's GPU half described the latch that no longer
|
||||
exists.
|
||||
|
||||
**Owed outright: nothing. The implementation and its evidence are
|
||||
complete.** The original pre-PR gate is green at `04ebd2f`; after the
|
||||
first PR review's two implementation fixes, the full review gate is
|
||||
green at code head `ef25920`, run `20260902T180422Z-311523`. PR #247
|
||||
remains open and unmerged.
|
||||
|
||||
### PR #247 review 1 — presentation and direct panel replacement
|
||||
|
||||
Two findings were implementation defects, not documentation residuals.
|
||||
|
||||
- **B2 changed `code_scroll_left` without requesting a redraw.** The
|
||||
event loop returns to `ControlFlow::Wait`, and horizontal document
|
||||
scrolling deliberately emits no viewport wire event. The stored
|
||||
origin therefore could remain invisible until unrelated input. The
|
||||
new production-path row asserts both halves independently: the origin
|
||||
changes and the redraw-request count advances exactly once. Removing
|
||||
the request leaves the first assertion green and fires the second.
|
||||
- **B1 disposed panel banks on `Absent`, but not on a direct accepted
|
||||
identity replacement.** `ResidualOwner::Panel(BufferId)` cannot
|
||||
distinguish a persistent buffer returning under a new `panel_epoch`,
|
||||
so A's fraction could survive A → B → A and be spent by the successor.
|
||||
Separate mapped and legacy rows drive the real producer through a
|
||||
same-buffer/new-epoch replacement with no `Absent`; omitting either
|
||||
branch's discard fires only that family. A geometry-only re-grid is
|
||||
the negative control: it is the same panel surface, preserves the
|
||||
bank, and fails if `geometry_epoch` is folded into wheel identity.
|
||||
|
||||
All four mutations were built and executed, not inferred from grep.
|
||||
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu` reports **326 passed**.
|
||||
The full 16-stage gate at `ef25920` passed fmt, workspace/all-targets
|
||||
clippy, lib, lib-crdt, all eight touched acceptance suites, m4, GPU,
|
||||
the workspace sweep and diff-check. Reading every stage log finds zero
|
||||
`FAILED` lines and zero non-zero-failure `test result:` lines.
|
||||
|
||||
The last three closed in order:
|
||||
|
||||
- **step 3's fractional both-axis panel witness** (`302ce14`, rewritten
|
||||
at `e50f38a`). The first version **counted emitted events**, which is
|
||||
the blind spot the framing exists to close — it says so directly:
|
||||
*"Not 'a `PanelPointer` was emitted' — the observable effect on the
|
||||
panel's viewport."* It now runs both halves in one row: this
|
||||
frontend's `apply_wheel` as producer, a real `EditorState` with a live
|
||||
panel window as receiver, and the panel's `(view_top, view_left)` as
|
||||
the assertion. **`pmacs-gpu` gained a DEV-dependency on `pmacs`** for
|
||||
it, and `pmacs` four `#[doc(hidden)]` test-support methods; the
|
||||
daemon's own `semantic_panel_view` delegates to one of them, so there
|
||||
is a single panel fixture rather than two. The mutation that settles
|
||||
it is the receiver-side one no emission count could see: dropping
|
||||
`PKind::ScrollLeft`/`ScrollRight` from the daemon's panel arm fires
|
||||
this row. Review found one residual at `a3b0bb8`: the row said
|
||||
**exactly one step** while asserting only that each origin became
|
||||
greater than zero. `9ec4ff1` pins the exact `(view_top, view_left)`
|
||||
after both completions, asserts the entire sub-threshold `Step` is
|
||||
empty, requires one correctly directed outbound event and no local
|
||||
effect on each completion, and asserts the receiver's geometry
|
||||
declaration returned `Advanced`. Doubling the vertical and horizontal
|
||||
receiver steps fails at `(6, 0)` and `(3, 6)` respectively; adding a
|
||||
local document scroll beside the panel event fails on the completion
|
||||
transcript. The focused row is green outside the socket-restricted
|
||||
sandbox; package-wide all-target clippy, fmt and `git diff --check`
|
||||
are clean. The code later passed the full pre-PR gate at `04ebd2f`;
|
||||
no further gate is owed before the PR.
|
||||
- **R4 and R5** (`e5ab16c`).
|
||||
- **B1's disposal half** (`241e82e` for panels, `a7006fa` for
|
||||
terminals) — and it needed mechanism, not just a row. `BufferId` keying distinguishes panel A from panel B for
|
||||
free, but **not a panel closed and reopened on the same persistent
|
||||
buffer**, where the successor carries the same key. The `Absent` arm
|
||||
reset eight pieces of panel state one line at a time with the wheel
|
||||
residual missing from the list — the same omission shape as the
|
||||
horizontal origin missing from the TUI's four replacement resets.
|
||||
|
||||
**B6 (`2dccc2b`, tightened in `f441d3d`)** is three rows on
|
||||
`EffectHarness`, the first production-path coverage `apply_wheel` has
|
||||
ever had: a notch over the minimap scrolls the document viewport, a
|
||||
part-notch over it cannot complete one over the document, and its
|
||||
horizontal axis is inert. Its mechanism had landed with B1; only the
|
||||
evidence was owed.
|
||||
|
||||
Its fixture is the recovery-relevant part. **Two separate conditions
|
||||
each make a horizontal-inertness claim vacuous**, and the default
|
||||
harness document has both: four-column lines pin B7's saturated right
|
||||
bound to zero, and line wrap — **on by default** — makes
|
||||
`scroll_by_columns` return early and pin the left edge, whatever the
|
||||
target. Under either, *nothing* scrolls sideways and a row asserting
|
||||
"the minimap did not" passes without testing anything. The row uses
|
||||
wide lines and turns wrap off through `LineWrapFacts`, asserting the
|
||||
wrap landed; `EffectHarness::new()` keeps its 200-line fixture and M22
|
||||
rationale untouched, with `with_document` added beside it.
|
||||
|
||||
Review then caught the same row asserting the absence of one effect and
|
||||
calling it inertness: it pinned neither probe's target (panel chrome is
|
||||
horizontally inert too, for unrelated reasons) and discarded both
|
||||
`Step`s, so an unchanged `code_scroll_left` was the whole claim. It now
|
||||
asserts both targets exactly and an empty transcript on the minimap
|
||||
leg. **The contrast leg cannot use the transcript** — a horizontal
|
||||
document scroll is local and silent, so both legs' transcripts are
|
||||
empty and only `code_scroll_left` separates them.
|
||||
|
||||
### Two gate gaps this lane found, neither fixed here
|
||||
|
||||
Both are shared infrastructure and belong in their own lane; recorded so
|
||||
the next lane does not rediscover them at review.
|
||||
|
||||
- **`scripts/gate`'s clippy step runs DEFAULT FEATURES only**, so no
|
||||
`#[cfg(feature = "crdt")]` code is ever linted locally. A
|
||||
`clippy::match_same_arms` on the CRDT lane's new enumeration passed
|
||||
five consecutive all-green gate runs and then redded `Test (crdt)`.
|
||||
- **NOTHING RUNS `cargo doc`** — not `scripts/gate`, not any `ci.yml`
|
||||
job — so **broken intra-doc links are ungated across this
|
||||
repository**. `git diff --check` cannot see them (they are
|
||||
syntactically valid) and clippy does not read them. Found when
|
||||
deleting a helper left a dangling `[`link`]` that review caught
|
||||
instead. Running it by hand also surfaces a pre-existing unresolved
|
||||
link, `MathNode` at `pmacs-gpu/src/math_layout.rs:314`.
|
||||
|
||||
### `scripts/gate` refuses to run under `nohup`
|
||||
|
||||
Not a red — a **refusal**, and worth recording because it looks like
|
||||
neither success nor failure. Launched with `nohup`, the gate printed
|
||||
|
||||
```
|
||||
pmacs: SIGINT is ignored; run this command with SIGINT deliverable
|
||||
gate: REFUSING TO RUN (status=1 token=valid) --- no stage has run.
|
||||
```
|
||||
|
||||
and exited in under a second. `nohup` leaves SIGINT ignored, and the
|
||||
gate declines to start in that state rather than run stages nobody can
|
||||
interrupt. **The guard is right**; the way it was hit is the lesson.
|
||||
Background the gate through the harness's own mechanism, not `nohup`.
|
||||
|
||||
**The expensive half was the watch, not the refusal.** A monitor was
|
||||
armed on the output file filtering for step banners and test results —
|
||||
so a gate that never started produced exactly what a gate still running
|
||||
produces: nothing. Fifteen minutes of silence read as progress. The
|
||||
question to ask before arming any watch is *if this process died right
|
||||
now, would my filter emit anything?*, and here the answer was no.
|
||||
|
||||
**A trailing `; echo` in the same launch also hid the exit code from
|
||||
the caller.** In
|
||||
`./scripts/gate … > log 2>&1; echo "exit=$?"`, `$?` is expanded before
|
||||
`echo` runs, so the text printed is the **gate's** status. The compound
|
||||
shell command itself, however, returns the later **echo's** status —
|
||||
normally zero — so the harness recorded exit 0 for a gate that failed
|
||||
at step 03. Preserve and re-emit the status explicitly, or read the
|
||||
gate's own summary line; do not treat the wrapper's status as the
|
||||
gate's.
|
||||
|
||||
**A later attempt was terminated by the harness, not by a test red.**
|
||||
Run `20260902T124755Z-4122046` reached step `15-sweep`; that stage's log
|
||||
contains two compilation lines followed by `Terminated`, with **zero
|
||||
`test result:` lines**, and the gate reported `FAILED (exit 143)`.
|
||||
The preceding fourteen stages had run successfully, but the sweep had
|
||||
not executed a test. This is evidence that the harness's background
|
||||
lifetime expired, not evidence about the tree. A monitor must observe
|
||||
process exit as well as filtered gate output, and the launch mechanism
|
||||
must outlive the full gate.
|
||||
|
||||
### A local false compile red from the shared target directory
|
||||
|
||||
Kept here rather than in `docs/ci-red-signatures.md` because it is a
|
||||
**machine-local artifact-state hazard with no CI occurrence** — not
|
||||
because the registry cannot represent local reds. It plainly can: it
|
||||
carries many `local (Linux)` rows with precise invocation flavors, one
|
||||
of them (the `04-lib-crdt` pair) even flavored *"with sibling worktrees
|
||||
building concurrently."* This incident's truthful flavor would be
|
||||
**local, bare `cargo test -p pmacs-gpu`, globally shared target**, and a
|
||||
row could state it exactly. An earlier draft of this entry claimed the
|
||||
registry was for CI reds only and that a row would have to invent a job
|
||||
and flavor; that was false, and review caught it.
|
||||
|
||||
What actually keeps it out is that the registry is live triage policy
|
||||
for judging a red *run*, and every occurrence of this one has been on
|
||||
this machine, from bypassing `scripts/gate`. **If it ever appears in
|
||||
CI, that is a new incident and belongs in the registry under the
|
||||
actual job and flavor that observed it** — not under this local
|
||||
flavor. This evidence neither predicts nor excludes such an
|
||||
occurrence. An earlier correction tried to keep that possibility open
|
||||
by pointing at `Swatinem/rust-cache@v2`, but that mechanism was not
|
||||
verified: the action's default does not cache workspace crates, and
|
||||
GitHub cache visibility is scoped rather than shared indiscriminately
|
||||
across branches. The unsupported mechanism is withdrawn instead of
|
||||
being replaced with the equally unverified claim that CI builds fresh.
|
||||
|
||||
It has now fired **twice in this lane**.
|
||||
|
||||
**Signature.** From `cargo test -p pmacs-gpu`, four errors against the
|
||||
bin `pmacs-gpu` test target:
|
||||
|
||||
```
|
||||
error[E0433]: cannot find `columns` in `pmacs_protocol`
|
||||
--> pmacs-gpu/src/main.rs:13291:21
|
||||
error: could not compile `pmacs-gpu` (bin "pmacs-gpu" test)
|
||||
due to 4 previous errors
|
||||
```
|
||||
|
||||
**FOUR occurrences now**, all local. The third (at `9cb610e`) gave the
|
||||
complete set of four sites, which the second's captured tail had cut to
|
||||
three: at that head they were `main.rs:9049`, `:13546`, `:14772` and
|
||||
`:14833` — every use of the module the 1b branch added in `9e54cd2`.
|
||||
The fourth (2026-09-02, mid-edit) hit the same four uses at
|
||||
`:9109`, `:13658`, `:14884` and `:14945`. **Line numbers drift as the
|
||||
file grows and are not part of the signature; the module path is**, and
|
||||
the count of four is stable across all of them.
|
||||
|
||||
An earlier draft of this entry guessed the missing fourth site was
|
||||
`widest_display_columns`. The third occurrence shows that guess was
|
||||
right — and it was still right not to record it, because a signature
|
||||
that is *usually* right is one nobody can match against.
|
||||
|
||||
**Why it is false.** Both times, the module was fully present:
|
||||
`pmacs-protocol/src/columns.rs` existed and matched HEAD, and
|
||||
`pub mod columns;` stood at `pmacs-protocol/src/lib.rs:40` in both the
|
||||
worktree and HEAD. Nothing was missing to find.
|
||||
|
||||
**What it actually is.** `CARGO_TARGET_DIR` is exported globally on this
|
||||
machine — `/home/jeans/build/cargo-target`, one directory shared by
|
||||
every checkout — and `main` has no `columns` module at all (the sharing
|
||||
refactor is unmerged 1b work). A `pmacs-protocol` artifact built without
|
||||
the module therefore sits in the same target directory as the branch
|
||||
that needs it, and gets reused. The confirmed facts are the shared
|
||||
directory, the module's absence on `main`, and that the red cleared;
|
||||
which build deposited the stale artifact — the `main` checkout, or this
|
||||
worktree before `9e54cd2` — was not determined, and either has the same
|
||||
shape.
|
||||
|
||||
**Control.** `touch pmacs-protocol/src/lib.rs` forces the rebuild and
|
||||
clears it. **But check before touching**: confirm `pub mod columns;` and
|
||||
`columns.rs` are actually present and match HEAD first, because the
|
||||
touch would just as happily paper over a real deletion, and this red is
|
||||
indistinguishable by eye from that one. The durable fix is not to touch
|
||||
at all but to run **`scripts/gate`**, whose per-worktree
|
||||
`CARGO_TARGET_DIR` is exactly this problem's remedy — see
|
||||
`docs/agent-handoff.md:2609`, which documents the general bare-cargo
|
||||
hazard and the environment variable's override of `build.target-dir`,
|
||||
but not this signature, its count, or its control.
|
||||
|
||||
**The cost of misreading it.** It presents as a compile error naming a
|
||||
missing module, which reads as a real regression — a deleted file, a
|
||||
dropped `mod` line, a bad rebase. Both times the first instinct was to
|
||||
look for what had been removed. Nothing had been.
|
||||
|
||||
### And one coverage gap in CI, which shaped B4's design
|
||||
|
||||
**No non-Linux leg runs this crate's tests.** `cargo test -p pmacs-gpu`
|
||||
appears exactly once in `ci.yml`, in the Ubuntu-only `gpu-render` job;
|
||||
the macOS matrix tests the workspace default member only. B4's
|
||||
off-Linux contract would therefore have been asserted nowhere that
|
||||
executes. **Production still reads `cfg!(target_os = "linux")`**
|
||||
(`pmacs-gpu/src/main.rs:3450`); what became injectable is the
|
||||
**selection decision taken after** that target fact — `is_linux` is
|
||||
obtained from `cfg!` in a non-test build and from a test override
|
||||
otherwise, and `paste_source_for(is_linux)` is the part a row can drive
|
||||
both ways. Adding a macOS `pmacs-gpu` leg is the real fix and is not
|
||||
this lane's.
|
||||
|
||||
## CRDT identity-replace undo — MERGED as #246 (`78346de`)
|
||||
|
||||
- **MERGED 2026-08-31T18:56:09Z** at approved head `093d677`, merge
|
||||
|
|
|
|||
|
|
@ -1083,6 +1083,78 @@ regression in two unrelated subsystems at once is far less likely than
|
|||
one loaded machine. If a future run reds **one** of these without the
|
||||
other, that is a different incident and should be judged as one.
|
||||
|
||||
### U20 — U6's composition test reds ALONE, which U6 says is a new incident
|
||||
|
||||
Recorded during GUI Stage 1b, 2026-09-01, local (Linux). **This row
|
||||
exists because U6's closing rule says it must**: *"If a future run reds
|
||||
one of these without the other, that is a different incident and should
|
||||
be judged as one."* This is that run. It is filed separately rather than
|
||||
as a sixth U6 occurrence, because U6's selector requires **both** tests
|
||||
failing together and its whole argument rests on the pair — a real
|
||||
regression in two unrelated subsystems at once being less likely than
|
||||
one loaded machine. One test alone does not carry that argument.
|
||||
|
||||
| field | value |
|
||||
|---|---|
|
||||
| **selector** | `--lib editor::tests::composition_overhead_under_ten_percent`, **alone** — `optimistic::tests::criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke` passed in the same run |
|
||||
| **job / flavor** | local (Linux), bare `cargo test --lib` (default features), full-lib load, **not** `scripts/gate` |
|
||||
| **required fragments** | `composition machinery added more than 10% overhead` + a ratio; the run also printed `dispatch overhead : 87.9%` and `realistic overhead : 82.1%` |
|
||||
| **status** | **ONE occurrence.** Not reproduced in six subsequent full-lib runs — three on the observing tree and three on the same tree with the observing diff reverted |
|
||||
| **margin** | **1.879 against a 1.10 budget** (`single=158789 ns, dispatch=298366 ns`). Recorded because U11 taught this registry what a missing margin costs. It is the largest this selector has shown: U6's were 1.297, 1.182, 1.592, 1.527 and one unrecorded. Against the previous worst of 1.592: **1.18× as a ratio**; **1.48× as much overhead above the 1.0 no-overhead baseline** (0.879 vs 0.592); **1.58× as much excess over the 1.10 budget** (0.779 vs 0.492). Three numbers for three questions, which is why none is compressed into an adjective — an earlier draft called the middle one "budget excess", which it is not: the budget is 1.10, not 1.0 |
|
||||
| **what IS established** | the observing diff (`9cb610e`, the TUI horizontal-authority latch) adds work to the paint path, so it was a live suspect. Three full-lib runs with the diff and three with `src/editor.rs` and `src/window.rs` restored to HEAD: **all six green.** That establishes **non-reproduction in six runs, and nothing more** — this registry's own rule is that reruns establish intermittence only |
|
||||
| **what is NOT** | why it fired. The load confound is once again unmeasured — no `/proc/loadavg` reading was taken at the moment of failure, which is the same gap U15 exists to close |
|
||||
|
||||
**The margin is the part that does not fit.** 1.879 is not a budget
|
||||
missed by a hair; it is the machinery costing nearly twice what the
|
||||
budget allows. A load-sensitive assertion can produce that, but so can
|
||||
a real regression, and this row does not pretend the size of the margin
|
||||
resolves the question.
|
||||
|
||||
**Nor do the six controlled runs.** An earlier draft of this row said
|
||||
they establish that the observing diff is not the cause. They do not: a
|
||||
tree that fails intermittently can carry a *changed failure rate* that
|
||||
six runs are far too few to see, and treating non-reproduction as
|
||||
exoneration is precisely the reasoning this registry refuses when a
|
||||
rerun turns a red green. What the controls do is remove the easy
|
||||
story — "the diff obviously did it" — and leave the question open.
|
||||
|
||||
**If this selector reds alone again, this row is where it goes**, and
|
||||
the first thing to capture is a contemporaneous load reading — the one
|
||||
piece of evidence that would separate the two explanations and that
|
||||
neither U6 nor this row has at the moment of failure.
|
||||
|
||||
### U21 — the canonical PTY test reds ENTIRELY alone, in `03-lib`
|
||||
|
||||
Recorded during GUI Stage 1b's pre-PR gate, 2026-09-02, local (Linux).
|
||||
**A new incident by U6's rule and U9's, not a fourth occurrence of
|
||||
either** — see the relation cells below, which are the point of the row.
|
||||
|
||||
| field | value |
|
||||
|---|---|
|
||||
| **selector** | `--lib process::tests::m6_1_pty_canonical_mode_keeps_kernel_echo`, **alone** — no second selector redded anywhere in the run |
|
||||
| **job / flavor** | local (Linux), `scripts/gate` step `03-lib`, per-worktree target dir, inside a full gate |
|
||||
| **required fragments** | ``canonical mode should leave echo enabled (no `-echo` flag); stty -a output was: ""`` |
|
||||
| **NOT fragments** | the `:LINE` suffix (`src/process.rs:3981` at this head) and the pass/fail counts — both occurrence-specific |
|
||||
| **status** | **one occurrence**, `2008 passed; 1 failed; 3 ignored` |
|
||||
| **what IS established — two IN-RUN repetitions** | the same selector **passed in step `04-lib-crdt` and step `15-sweep` of the SAME gate run**, on the same tree, minutes after the step-03 red. This establishes within-run intermittence and excludes a deterministic failure on that tree. It does **not** exclude the tree as a cause of an intermittent failure or a changed failure rate. Also green in three subsequent full `--lib` runs and once isolated (`1 passed`, 0.01s) |
|
||||
| **what is NOT** | cause. **The load confound is once again not measured at the moment of failure** — the readings that exist were taken afterwards: 1-minute 4.15, 4.38, 6.43, 6.54 with the 5-minute figure at 9.6–10.0, i.e. a genuinely busy machine, but *after* the fact. This is the same gap U15 exists to close and it is still open |
|
||||
| **relation to U2 — same family, different selector** | U2's fragment is identical (`stty -a output was: ""`) but its selector names `m6_1_pty_raw_mode_disables_kernel_echo`. Raw passed here |
|
||||
| **relation to U9 — the closest row, and still not a match** | U9's selector requires the canonical PTY test **and** `composition_overhead_under_ten_percent` failing together in `11-sweep`. Here the PTY test redded **with no companion at all**, in `03-lib`, and the composition test passed. U9's own framing distinguishes "canonical alone" from *raw*; this is canonical alone from *everything*, which no existing row has shown |
|
||||
|
||||
**What this adds to the family is the isolation.** Every prior
|
||||
occurrence of this fragment came paired — raw with canonical (U2's
|
||||
second), or canonical with a budget test (U9). Here one PTY assertion
|
||||
failed by itself in the narrowest step, then the same selector passed
|
||||
twice later in the same gate. The fragment establishes only that **the
|
||||
supervisor collected empty stdout**; it shows no incorrect termios
|
||||
state and does not distinguish among {the child never wrote, PTY
|
||||
delivery lost the bytes, event collection missed them}. That is the
|
||||
same evidence boundary U2 records.
|
||||
|
||||
**Do not fold this into U2 or U9.** Both rows exist because their
|
||||
authors resisted the same pull, and each records a combination the
|
||||
others cannot see.
|
||||
|
||||
### U7 — a *different* wall-clock render-budget test reds each sweep
|
||||
|
||||
Recorded during worker identity Stage 1 review round 3, 2026-08-09.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -70,3 +70,8 @@ unicode-width = "0.2"
|
|||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
# TEST-ONLY, and never a runtime dependency: step 3's panel-wheel
|
||||
# witness has to observe the RECEIVER's effect, not the event this
|
||||
# frontend emits, because the defect it exists for is precisely
|
||||
# "the frontend emits and the receiver discards".
|
||||
pmacs = { path = ".." }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,82 @@
|
|||
// columns.rs --- The display-column rule both frontends reckon in.
|
||||
|
||||
//! One definition of "a display column", shared across the wire.
|
||||
//!
|
||||
//! The TUI and the GPU both need to answer "how wide is this line?" —
|
||||
//! for the caret follow, for the minimap, and for GUI Stage 1b's B7
|
||||
//! right bound. **Two copies of that answer is a defect waiting to
|
||||
//! happen**: a bound computed one way and a follow computed the other
|
||||
//! disagree about where the document ends, and the disagreement is
|
||||
//! invisible until a tab or a wide character reaches the edge.
|
||||
//!
|
||||
//! It lives here for the same reason [`crate::scroll::follow_left`]
|
||||
//! does — the protocol crate is the one place both frontends already
|
||||
//! depend on.
|
||||
//!
|
||||
//! **Scope: SOURCE-TEXT columns.** Tab stops and Unicode terminal
|
||||
//! width. Rendered projections — inline adornments, math substitutions
|
||||
//! — can occupy a different width on screen and are deliberately not
|
||||
//! counted here.
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// Advance `column` past one character.
|
||||
///
|
||||
/// A tab reaches the next [`crate::TAB_STOP_COLUMNS`] stop; every other
|
||||
/// character contributes its Unicode terminal width, so control and
|
||||
/// zero-width characters do not advance.
|
||||
#[must_use]
|
||||
pub fn advance_char(column: u32, ch: char) -> u32 {
|
||||
let width = if ch == '\t' {
|
||||
crate::TAB_STOP_COLUMNS - (column % crate::TAB_STOP_COLUMNS)
|
||||
} else {
|
||||
UnicodeWidthChar::width(ch).unwrap_or(0) as u32
|
||||
};
|
||||
column.saturating_add(width)
|
||||
}
|
||||
|
||||
/// Display width of one line, in columns.
|
||||
#[must_use]
|
||||
pub fn line_columns(line: &str) -> u32 {
|
||||
line.chars().fold(0, advance_char)
|
||||
}
|
||||
|
||||
/// Widest line in `text`, in display columns — B7's right-bound input.
|
||||
#[must_use]
|
||||
pub fn widest_line_columns(text: &str) -> u32 {
|
||||
text.split('\n').map(line_columns).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{advance_char, line_columns, widest_line_columns};
|
||||
|
||||
#[test]
|
||||
fn a_tab_reaches_the_next_stop_rather_than_advancing_one() {
|
||||
assert_eq!(advance_char(0, '\t'), crate::TAB_STOP_COLUMNS);
|
||||
assert_eq!(advance_char(1, '\t'), crate::TAB_STOP_COLUMNS);
|
||||
assert_eq!(
|
||||
advance_char(crate::TAB_STOP_COLUMNS, '\t'),
|
||||
crate::TAB_STOP_COLUMNS * 2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_and_zero_width_characters_are_measured_not_counted() {
|
||||
assert_eq!(line_columns("ab"), 2);
|
||||
assert_eq!(line_columns("漢字"), 4, "wide characters take two columns");
|
||||
assert_eq!(line_columns("a\u{200b}b"), 2, "zero-width adds nothing");
|
||||
}
|
||||
|
||||
/// The widest line, not the last one and not the first.
|
||||
#[test]
|
||||
fn widest_line_is_the_maximum_over_all_lines() {
|
||||
assert_eq!(widest_line_columns("a\nbbbb\ncc"), 4);
|
||||
assert_eq!(widest_line_columns(""), 0);
|
||||
assert_eq!(
|
||||
widest_line_columns("\tx"),
|
||||
crate::TAB_STOP_COLUMNS + 1,
|
||||
"tabs count toward the bound"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
//! directly.
|
||||
|
||||
pub mod cell;
|
||||
pub mod columns;
|
||||
pub mod crdt;
|
||||
pub mod ids;
|
||||
pub mod message;
|
||||
|
|
|
|||
212
src/daemon.rs
212
src/daemon.rs
|
|
@ -3847,6 +3847,7 @@ fn align_primary_document_window(
|
|||
win.cursor = 0;
|
||||
win.selection = None;
|
||||
win.overlays.clear();
|
||||
win.forget_manual_horizontal_origin();
|
||||
}
|
||||
Some(win_id)
|
||||
}
|
||||
|
|
@ -5556,6 +5557,98 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// GUI Stage 1b, lifetime clause 5 — **the daemon's alignment path
|
||||
/// is a buffer replacement too**, and must forget a manual
|
||||
/// horizontal origin like the other two.
|
||||
///
|
||||
/// `align_primary_document_window` re-points a window at the buffer
|
||||
/// its frontend declares. That is a replacement by any measure: a
|
||||
/// sideways origin carried across it renders the successor scrolled
|
||||
/// with nothing about that buffer to explain it. This row lives
|
||||
/// here rather than beside L8b/L8c because the function is private
|
||||
/// to this module.
|
||||
///
|
||||
/// The latch is armed the production way — a real wheel gesture
|
||||
/// through `dispatch_mouse` — not by writing the fields, so the row
|
||||
/// cannot pass against a state the running editor never reaches.
|
||||
///
|
||||
/// **Not `crdt`-gated**, unlike its neighbour above: nothing here
|
||||
/// needs the feature, and gating it would keep it out of the
|
||||
/// default `--lib` leg for no reason — the same blind spot that
|
||||
/// already lets `crdt`-only code go unlinted locally.
|
||||
///
|
||||
/// *Mutation: drop the `forget_manual_horizontal_origin()` call
|
||||
/// from `align_primary_document_window` → this row.*
|
||||
#[test]
|
||||
fn l8d_the_alignment_path_clears_a_manual_horizontal_origin() {
|
||||
use crate::editor::EditorState;
|
||||
use crate::protocol::FrontendId;
|
||||
use crossterm::event::{KeyModifiers, MouseEvent, MouseEventKind};
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let wide = |name: &str| {
|
||||
let core = editor.core.borrow();
|
||||
let mut content = b"wide\n".to_vec();
|
||||
content.extend_from_slice(&b"w".repeat(400));
|
||||
content.push(b'\n');
|
||||
core.registry
|
||||
.borrow_mut()
|
||||
.create_from_bytes(name.to_owned(), &content)
|
||||
};
|
||||
let first = wide("first");
|
||||
let second = wide("second");
|
||||
let fid = FrontendId(99);
|
||||
let view = build_fresh_frontend_view(&mut editor, false, false);
|
||||
editor.core.borrow_mut().register_frontend_view(fid, view);
|
||||
|
||||
// The window starts on LOCAL's narrow scratch buffer, where
|
||||
// B7's `widest − viewport` bound is zero and every notch is
|
||||
// absorbed. Put a wide buffer under it first — through the very
|
||||
// function under test — so the gesture below can be effective.
|
||||
align_primary_document_window(&mut editor, fid, first);
|
||||
|
||||
for _ in 0..10 {
|
||||
editor.dispatch_mouse(
|
||||
fid,
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::ScrollRight,
|
||||
column: 5,
|
||||
row: 5,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
crate::cell::CellSize::new(24, 80),
|
||||
);
|
||||
}
|
||||
let armed = editor
|
||||
.core
|
||||
.borrow()
|
||||
.active_window_for(fid)
|
||||
.expect("the semantic frontend has a window")
|
||||
.view_left;
|
||||
assert!(
|
||||
armed > 0
|
||||
&& editor
|
||||
.core
|
||||
.borrow()
|
||||
.active_window_for(fid)
|
||||
.expect("window")
|
||||
.manual_left_authority,
|
||||
"setup: a real wheel gesture must have moved the origin and \
|
||||
armed authority, else this row measures nothing"
|
||||
);
|
||||
|
||||
align_primary_document_window(&mut editor, fid, second);
|
||||
|
||||
let win = editor.core.borrow();
|
||||
let win = win.active_window_for(fid).expect("window");
|
||||
assert_eq!(
|
||||
win.view_left, 0,
|
||||
"the successor must not inherit the predecessor's sideways \
|
||||
viewport"
|
||||
);
|
||||
assert!(!win.manual_left_authority, "nor the authority defending it");
|
||||
}
|
||||
|
||||
/// B1 input/display alignment: a semantic frontend's window is bound
|
||||
/// to LOCAL's attach-time buffer, but the buffer it *displays* is
|
||||
/// the one it declares via `Viewport`. `align_primary_document_window`
|
||||
|
|
@ -6323,58 +6416,9 @@ mod tests {
|
|||
fid: FrontendId,
|
||||
with_panel: bool,
|
||||
) -> (crate::window::WindowId, Option<crate::window::WindowId>) {
|
||||
use crate::window::{FrontendView, Layout, LayoutNode, Orientation, Window, WindowParams};
|
||||
|
||||
let mut core = editor.core.borrow_mut();
|
||||
let doc_buf = core.active_window().buffer_id;
|
||||
let document = crate::window::WindowId::next();
|
||||
let doc_view = {
|
||||
let reg = core.registry.borrow();
|
||||
crate::text_view::TextView::new(reg.get(doc_buf).expect("doc"))
|
||||
};
|
||||
core.windows
|
||||
.insert(document, Window::new(document, doc_buf, doc_view));
|
||||
let panel = with_panel.then(|| {
|
||||
let panel_buf = core.registry.borrow_mut().create("*panel*");
|
||||
let panel_id = crate::window::WindowId::next();
|
||||
let panel_view = {
|
||||
let reg = core.registry.borrow();
|
||||
crate::text_view::TextView::new(reg.get(panel_buf).expect("panel"))
|
||||
};
|
||||
let mut window = Window::new(panel_id, panel_buf, panel_view);
|
||||
let mut params = WindowParams::default();
|
||||
params.side = Some(crate::window::Side::Bottom);
|
||||
params.fixed_rows = Some(4);
|
||||
window.params = params;
|
||||
core.windows.insert(panel_id, window);
|
||||
panel_id
|
||||
});
|
||||
let layout = match panel {
|
||||
Some(panel) => Layout {
|
||||
root: LayoutNode::Split {
|
||||
orientation: Orientation::Horizontal,
|
||||
children: vec![LayoutNode::Leaf(document), LayoutNode::Leaf(panel)],
|
||||
weights: vec![1, 1],
|
||||
},
|
||||
},
|
||||
None => Layout::single(document),
|
||||
};
|
||||
core.register_frontend_view(
|
||||
fid,
|
||||
FrontendView {
|
||||
layout,
|
||||
active: document,
|
||||
fold_projection: false,
|
||||
// Stage 2B-2 is dark: production negotiation still sets
|
||||
// this `false` for every semantic session, so the
|
||||
// projection is exercised through a test-only view (the
|
||||
// framing's §7.2.2 posture).
|
||||
panel_capable: true,
|
||||
frame_geometry: None,
|
||||
panel_hidden: false,
|
||||
},
|
||||
);
|
||||
(document, panel)
|
||||
// One fixture, shared with `pmacs-gpu`'s step-3 effect witness,
|
||||
// which needs a real panel window to observe an effect on.
|
||||
editor.install_panel_view_for_test(fid, with_panel)
|
||||
}
|
||||
|
||||
fn session(version: u32, semantic: bool) -> crate::presence::SessionState {
|
||||
|
|
@ -7671,6 +7715,70 @@ mod tests {
|
|||
editor.core.borrow().windows[&panel].cursor
|
||||
}
|
||||
|
||||
/// GUI Stage 1b B2 — a horizontal panel notch reaches the daemon's
|
||||
/// window-targeted `view_left` path and MOVES it.
|
||||
///
|
||||
/// This arm used to be claimed and dropped, which is the
|
||||
/// "frontend emits, receiver discards" shape §2a named. The row
|
||||
/// asserts the **effect**, not the emission: `view_left` before and
|
||||
/// after.
|
||||
#[test]
|
||||
fn b2_a_horizontal_panel_notch_moves_the_side_windows_view_left() {
|
||||
let fid = FrontendId(791);
|
||||
let (mut editor, mut states, mut render, _document, panel, epochs) =
|
||||
panel_session_at(PROTOCOL_VERSION, fid);
|
||||
// A CONTENT cell, not chrome: row 0 is inside the grid.
|
||||
let (buffer_id, cell) = {
|
||||
let core = editor.core.borrow();
|
||||
(
|
||||
core.windows[&panel].buffer_id,
|
||||
pmacs_protocol::CellCoord::new(0, 0),
|
||||
)
|
||||
};
|
||||
// **The discriminating setup.** B7's bound is
|
||||
// `widest − viewport`, so a panel whose content fits has a
|
||||
// maximum origin of zero and the move is absorbed by the
|
||||
// clamp — correct behaviour that would read here as a dropped
|
||||
// event. The row needs content wider than the viewport.
|
||||
foreign_edit(&editor, buffer_id, "x".repeat(400).as_bytes());
|
||||
let before = editor
|
||||
.core
|
||||
.borrow()
|
||||
.windows
|
||||
.get(&panel)
|
||||
.map_or(0, |w| w.view_left);
|
||||
|
||||
let generation = live_generation(PanelArm::Mapped, &editor, &mut states, fid);
|
||||
dispatch_panel_event(
|
||||
&mut editor,
|
||||
fid,
|
||||
PROTOCOL_VERSION,
|
||||
&mut states,
|
||||
&mut render,
|
||||
arm_pointer(
|
||||
PanelArm::Mapped,
|
||||
fid,
|
||||
epochs,
|
||||
buffer_id,
|
||||
generation,
|
||||
cell,
|
||||
pmacs_protocol::MouseKind::ScrollRight,
|
||||
),
|
||||
);
|
||||
|
||||
let after = editor
|
||||
.core
|
||||
.borrow()
|
||||
.windows
|
||||
.get(&panel)
|
||||
.map_or(0, |w| w.view_left);
|
||||
assert_ne!(
|
||||
after, before,
|
||||
"a horizontal panel notch must move the side window's origin, \
|
||||
not be claimed and dropped"
|
||||
);
|
||||
}
|
||||
|
||||
/// P1 — a press on the band's MODE LINE begins nothing.
|
||||
///
|
||||
/// The merge made this arm the latch, because `Consumed` and
|
||||
|
|
|
|||
|
|
@ -7,20 +7,23 @@
|
|||
//! stop. Offsets are clamped to the supplied slice and offsets inside a UTF-8
|
||||
//! code point resolve to the preceding complete-code-point boundary.
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// Advance `column` past one character.
|
||||
///
|
||||
/// A tab reaches the next protocol tab stop; all other characters use their
|
||||
/// Unicode terminal width. Control and zero-width characters do not advance.
|
||||
/// **Delegates to [`pmacs_protocol::columns::advance_char`]**, which is
|
||||
/// where the rule lives so both frontends cannot drift apart on what a
|
||||
/// column is. This re-export keeps the existing call sites.
|
||||
#[must_use]
|
||||
pub fn advance_char(column: u32, ch: char) -> u32 {
|
||||
let width = if ch == '\t' {
|
||||
pmacs_protocol::TAB_STOP_COLUMNS - (column % pmacs_protocol::TAB_STOP_COLUMNS)
|
||||
} else {
|
||||
UnicodeWidthChar::width(ch).unwrap_or(0) as u32
|
||||
};
|
||||
column.saturating_add(width)
|
||||
pmacs_protocol::columns::advance_char(column, ch)
|
||||
}
|
||||
|
||||
/// Widest line in `text`, in display columns — B7's right bound.
|
||||
///
|
||||
/// **Delegates to [`pmacs_protocol::columns::widest_line_columns`]**,
|
||||
/// which both frontends call.
|
||||
#[must_use]
|
||||
pub fn widest_line_columns(text: &str) -> u32 {
|
||||
pmacs_protocol::columns::widest_line_columns(text)
|
||||
}
|
||||
|
||||
/// Display width of the valid UTF-8 prefix of `bytes`.
|
||||
|
|
|
|||
1217
src/editor.rs
1217
src/editor.rs
File diff suppressed because it is too large
Load Diff
|
|
@ -4075,6 +4075,7 @@ impl EditorCore {
|
|||
window.cursor = 0;
|
||||
window.selection = None;
|
||||
window.view_top = 0;
|
||||
window.forget_manual_horizontal_origin();
|
||||
window.goal_col = None;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -5415,6 +5416,7 @@ impl EditorCore {
|
|||
win.cursor = 0;
|
||||
win.selection = None;
|
||||
win.view_top = 0;
|
||||
win.forget_manual_horizontal_origin();
|
||||
win.goal_col = None;
|
||||
}
|
||||
}
|
||||
|
|
@ -5650,6 +5652,7 @@ impl EditorCore {
|
|||
aw.cursor = 0;
|
||||
aw.selection = None;
|
||||
aw.view_top = 0;
|
||||
aw.forget_manual_horizontal_origin();
|
||||
aw.goal_col = None;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -384,6 +384,24 @@ pub struct Window {
|
|||
/// Always `0` while this window's buffer wraps; see
|
||||
/// [`LayoutCtx::effective_left`](crate::view::LayoutCtx::effective_left).
|
||||
pub view_left: u32,
|
||||
/// GUI Stage 1b, lifetime clause 2 — **the user's horizontal origin
|
||||
/// outranks the caret's**.
|
||||
///
|
||||
/// Set when a deliberate horizontal scroll *effectively* moves
|
||||
/// [`Self::view_left`]; while set, the caret-following pass
|
||||
/// re-clamps the origin but does not drag it back. Without it a
|
||||
/// sideways wheel is undone by the very next paint, because
|
||||
/// `horizontal_follow` runs on every frame and knows only the
|
||||
/// caret.
|
||||
///
|
||||
/// Cleared by a genuine cursor move (clause 4), by wrap, and by
|
||||
/// buffer replacement (clause 5).
|
||||
pub manual_left_authority: bool,
|
||||
/// The cursor as it stood when [`Self::manual_left_authority`] was
|
||||
/// armed, so clause 4 can tell a *genuine* cursor change from the
|
||||
/// follow merely running again. Meaningless while the latch is
|
||||
/// clear.
|
||||
pub manual_left_cursor: Position,
|
||||
/// Sticky display column for vertical motion.
|
||||
pub goal_col: Option<u32>,
|
||||
/// Number of text rows that fit in this window's viewport at last
|
||||
|
|
@ -439,6 +457,8 @@ impl Window {
|
|||
selection: None,
|
||||
view_top: 0,
|
||||
view_left: 0,
|
||||
manual_left_authority: false,
|
||||
manual_left_cursor: 0,
|
||||
goal_col: None,
|
||||
last_visible_rows: 0,
|
||||
last_content_cols: 0,
|
||||
|
|
@ -467,6 +487,42 @@ impl Window {
|
|||
}
|
||||
}
|
||||
|
||||
/// Forget a manual horizontal origin because this window is
|
||||
/// adopting a **different buffer** (lifetime clause 5).
|
||||
///
|
||||
/// The origin is a fact about the document being shown, not about
|
||||
/// the window. Carried into a successor it renders the new buffer
|
||||
/// scrolled sideways with nothing about that buffer to explain it,
|
||||
/// until some later cursor motion repairs it by accident. The GPU
|
||||
/// learned this once already — `code_scroll_left` has its own line
|
||||
/// in that frontend's replacement reset, added after exactly this
|
||||
/// symptom — and the TUI's four replacement paths had neither the
|
||||
/// origin reset nor the latch clear.
|
||||
///
|
||||
/// One helper rather than a copy per site, so a new replacement
|
||||
/// path gets the rule by calling it; each **call site** stays
|
||||
/// individually removable, which is what keeps its own row honest.
|
||||
///
|
||||
/// **The census, taken by grepping every write of a window's
|
||||
/// `buffer_id` rather than by recalling which paths exist** — an
|
||||
/// earlier version of this doc said "three" and was wrong, because
|
||||
/// it listed the paths someone had thought of. Four production
|
||||
/// sites rebind a live window to a different buffer:
|
||||
/// `EditorCore::switch_active_buffer_for`,
|
||||
/// `EditorCore::install_buffer_in_window`,
|
||||
/// `EditorCore::kill_buffer`'s fallback rebind, and the daemon's
|
||||
/// `align_primary_document_window`. Each has its own row
|
||||
/// (L8b–L8e).
|
||||
///
|
||||
/// `EditorCore::from_bytes` also assigns `buffer_id`, and is
|
||||
/// **deliberately not on that list**: it builds a fresh core whose
|
||||
/// window has no prior origin to inherit. Named here so the next
|
||||
/// census does not have to re-decide it.
|
||||
pub fn forget_manual_horizontal_origin(&mut self) {
|
||||
self.view_left = 0;
|
||||
self.manual_left_authority = false;
|
||||
}
|
||||
|
||||
/// Width in cells this window's line-number gutter occupies, or `0`
|
||||
/// when disabled (UX gutter arc, Q#UX3). `digits(line_count) + PAD`;
|
||||
/// the renderer caps this against the window width and applies it as a
|
||||
|
|
|
|||
Loading…
Reference in New Issue