diff --git a/docs/active-work.md b/docs/active-work.md index 5d8d1d9..fb82f39 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -457,8 +457,9 @@ tip — the ref, not a SHA, since any edit to this block advances past whatever SHA it records. Recover: `git fetch githubsucks && git checkout horizontal-scroll`. -**Status: framing, NOT approved.** `docs/horizontal-scroll-framing.md` -revision 3. No implementation may begin. +**Status: `docs/horizontal-scroll-framing.md` revision 4 — every +question answered, APPROVAL NOT YET RECORDED. No implementation may +begin until it is.** **Answered by the user 2026-08-07:** @@ -476,43 +477,32 @@ revision 3. No implementation may begin. the TUI and a **dead end in the GUI** for anyone who never opened the setting. Revisit after Stage 5, on use evidence. -**Still blocking:** +- **Q#HS7 — ACCEPTED.** `view_left` is an unsnapped window display + column; the effective edge is derived **per line**; a bisected wide + glyph's trailing cell renders as styled blank and is designated to + the **glyph's start** byte; a straddling tab's surviving cells keep + the **existing** forward rounding to the byte after the tab + (`src/text_view.rs:224` — preserved, not chosen). Together these make + the mapping **total over visible cells**, which is the (d) invariant. + The discriminating witness is multi-line, with glyph widths differing + at the same column. +- **Q#HS5 — APPROVED: yes, persist, no `DESKTOP_VERSION` bump** — + conditional on `#[serde(default)]` **and** a literal v1 JSON fixture + omitting the field, asserting restore at zero. Both conditions are + part of the approval. -- **Q#HS7 (NEW) — what IS `view_left`?** Revision 1 decided what moves - the viewport without saying what its offset *is* — the same omission - as shipping `WrapMode` with no `DisplayCoord`. Without part (d), **a - painter that clips where the mapper does not puts clicks on the wrong - character**, silently, only on lines wide enough to scroll. - - **Revision 2's part (c) is WITHDRAWN.** It voted to snap `view_left` - to a valid boundary when set. That cannot exist: `view_left` is one - per-window display column, but *"does column N bisect a wide - glyph?"* is a **per-line** question — column 11 can be a wide glyph's - trailing cell on one line and ordinary ASCII on the next. No - setter-time value is canonical for every visible line, and snapping - per line instead would break vertical alignment. - - Replaced by a **per-line effective edge**: `view_left` stored - unsnapped, each line deriving its own edge in the walk it already - performs from column 0. Where the edge bisects a wide glyph, that - glyph's trailing cell **paints blank and the mapping designates it to - the glyph's start byte** — which keeps `byte_at_place` total and - preserves the round trip. The invariant is therefore a property of - **`(view_left, line)`**, not of `view_left` alone, so the oracle must - sweep lines whose glyph widths differ at the same column. -- **Q#HS5 — does `view_left` survive a restart?** Vote: yes, no - `DESKTOP_VERSION` bump — **conditional on two things, now concrete.** - Verified: `SavedLeaf` carries **no `#[serde(default)]` anywhere in - `src/desktop.rs`**, so serde would **reject** a version-1 desktop - JSON omitting a new `view_left`. Sound only with (1) - `#[serde(default)]` on the field and (2) a regression fixture — a - literal v1 desktop JSON without `view_left`, asserting restore at - offset 0 rather than an error. The reverse direction already works: - an old binary meets an unknown field, which serde ignores absent - `deny_unknown_fields` (none in that file). - **Q#HS3** is re-confirmed rather than open (per-window, per Q#LL2); **Q#HS4** is deferred, live only if explicit commands arrive. +**The reasoning worth keeping from the withdrawn Q#HS7(c).** Revision +2 voted to snap `view_left` to a valid boundary when set. That cannot +exist, and the reason generalizes: `view_left` is ONE per-window +column, but *"does column N bisect a wide glyph?"* is a **per-line** +question. No setter-time value is canonical for every visible line, +and snapping per line instead would break the vertical alignment a +column-oriented view exists to provide. Recorded because the same trap +waits for any future window-wide value derived from per-line content. + **One correction carried into revision 2.** Revision 1 claimed the "unreachable past the edge" caveat is recorded in the setting's description. It is not — `builtin/runtime/linewrap.lua:23` says only diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 60b530f..2029b9d 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -85,10 +85,10 @@ commands, read `docs/active-work.md` immediately after this file. ## 1. Where the project stands (2026-08-07) -- **QoL arc — Stages 1-3 merged (#219, #220, #221); Stage 4 is the - remainder.** From one daily-driver report: terminal zoom broke TUI - rendering and did nothing in the GUI, and a long line was unreadable - past the edge. +- **QoL arc — Stages 1-3 merged (#219, #220, #221); Stages 4 AND 5 + remain, and the arc closes at Stage 5.** From one daily-driver + report: terminal zoom broke TUI rendering and did nothing in the GUI, + and a long line was unreadable past the edge. - **#219** made the grid TUI honor `full_grid`, so a post-resize resync blanks the host before repainting. - **#220** gave the GUI native zoom over the font preference that @@ -125,10 +125,24 @@ commands, read `docs/active-work.md` immediately after this file. drawable clip. Neither `view_range` (it carries `SCROLL_OVERSCAN` past the window) nor `scroll_top` (it ignores `code_scroll_residual`) can answer it. - - **Stage 4 is horizontal scroll**, and `truncate` is incomplete - without it: text past the right edge is currently *unreachable*, - which is why `wrap` is the default and why the toggle says so. - Framing in `docs/horizontal-scroll-framing.md`. + - **Stage 4 is horizontal scroll in the TUI; Stage 5 is the GPU**, a + split decided rather than inherited (framing Q#HS1) and time-boxed: + Stage 5 is the immediately-next QoL lane after Stage 4 merges, and + `wrap` stays the default until it lands — which is what keeps the + divergence invisible to anyone who has not opted in. + + `truncate` is incomplete without scroll: text past the right edge is + currently *unreachable*. **That caveat is NOT in the setting's + description** — `builtin/runtime/linewrap.lua:23` says only + "truncate at the edge", and the word appears in + `ui.toggle-line-wrap`'s status message and a source comment, which + a user who sets the mode in `init.lua` never sees. A small + user-facing gap shipped in #221; amending the description is a + Stage 4 deliverable. + + **Rule 4 must not retire the long-lines lane when Stage 4 merges** — + the arc closes at Stage 5. Framing in + `docs/horizontal-scroll-framing.md`. - **`main` @ `db1bbe9`.** The **tree primitive #217** — `listview` rows take optional `depth`/`id`, collapse is primitive-owned, folding is diff --git a/docs/horizontal-scroll-framing.md b/docs/horizontal-scroll-framing.md index 97ea18d..f0e1476 100644 --- a/docs/horizontal-scroll-framing.md +++ b/docs/horizontal-scroll-framing.md @@ -1,10 +1,20 @@ # Horizontal scroll — QoL Stage 4 -**Status: revision 3 — NOT APPROVED. Q#HS1, HS2 and HS6 answered by the -user (2026-08-07). Q#HS7 remains BLOCKING and its part (c) is -**withdrawn and replaced** — a setter-time snap cannot exist for a -window-wide offset. Q#HS5's condition is now concrete. No -implementation may begin.** +**Status: revision 4 — every question answered; APPROVAL NOT YET +RECORDED. No implementation may begin until it is.** + +| question | state | +|---|---| +| Q#HS1 — GPU in scope? | **answered**: no, Stage 5, time-boxed (§3) | +| Q#HS2 — what moves the viewport? | **answered**: automatic only | +| Q#HS3 — window or buffer? | re-confirmed: per window | +| Q#HS4 — cursor follows explicit scroll? | **deferred** — not live under HS2 | +| Q#HS5 — persist `view_left`? | **approved**: yes, no version bump, on two conditions | +| Q#HS6 — the default | **answered**: `wrap` stays | +| Q#HS7 — what IS `view_left`? | **accepted**: (a), (b), (c′), (c″), (d) | + +Revision 4 adds only Q#HS7(c″) — the tab-straddle mapping — and fixes +the handoff's "Stage 4 is the remainder" to name Stages 4–5. **Stage 4 does NOT close the QoL arc.** Revision 1 said it did, and that was written before Q#HS1 moved GPU horizontal scroll to Stage 5. @@ -192,7 +202,13 @@ hazard for whenever explicit commands arrive. Deferring rather than deleting, because the hazard is real and rediscovering it costs more than carrying the paragraph. -### Q#HS5 — does `view_left` survive a restart? **OPEN, with the condition now concrete** +### Q#HS5 — does `view_left` survive a restart? **APPROVED: yes** + +> **Approved 2026-08-07 (user):** persist `view_left` **without** a +> desktop-version bump, **provided implementation adds +> `#[serde(default)]` and a literal v1 JSON fixture omitting the field, +> asserting restoration at zero.** Both conditions are part of the +> approval, not advice attached to it. `view_top` does (§1.4). Consistency argues yes. @@ -240,7 +256,13 @@ default does not move. Revisit after Stage 5, on use evidence, not before. -### Q#HS7 — what IS `view_left`? **NEW, BLOCKING** +### Q#HS7 — what IS `view_left`? **ACCEPTED (revision 4)** + +> **Accepted 2026-08-07 (user):** *"keep `view_left` as an unsnapped +> window display column; derive the effective edge per line; render a +> bisected wide glyph's trailing cell as styled blank and designate it +> to the glyph start. The multi-line discriminating witness is exactly +> right."* Plus (c″) below, on the user's recommendation. **Revision 1 decided what moves the viewport without ever saying what the viewport offset is.** That is the same omission Stage 3 would have @@ -327,6 +349,37 @@ pushed to the next row **entirely** (`advance_wrapped`, with its next row to push to, so the blank-plus-designation rule is what the same intent requires here. +**(c″) A tab whose expansion straddles the edge — PRESERVED, not +chosen.** + +Each visible tab-expansion cell maps to **the byte immediately after +the tab**. This is not a new rule: it is what `byte_at_place` already +does, and its doc comment says so — *"Rounds forward to the next +character boundary… matching the unwrapped `display_to_pos`"* +(`src/text_view.rs:224`). The walk accumulates `walked` past the tab +byte and returns on the *next* character's `start_col`, so every column +inside the expansion already yields the post-tab offset +(`src/text_view.rs:243-254`). + +So the requirement on Stage 4 is **that horizontal scroll not perturb +it**: with the expansion's leading cells scrolled off, the surviving +cells must still map post-tab, exactly as they do at offset 0. + +**Why this direction differs from (c′)'s, which is the obvious +objection.** A wide glyph's two cells belong to **one character**; +forward-rounding its trailing cell would designate it to the *next* +character and leave the straddling glyph with **no visible cell mapping +to it at all** — unreachable by click precisely when it is the thing +the user scrolled toward. A tab's expansion cells are whitespace +*between* the tab byte and the next character, and forward-rounding +them is already how clicking in indentation lands at the start of the +text. Different directions, one principle: **every visible cell is +designated to the byte a user would mean by clicking it.** + +With (c′) and (c″) together, the (d) contract is total over visible +cells: ordinary character → its own start byte; bisected wide glyph → +the glyph's start byte; tab expansion → the byte after the tab. + **(d) The invariant rendering and coordinate mapping share.** For a given `view_left` **and line**, `byte_at_place` must invert `place_of_byte` on every canonical input, and `paint_line` must place @@ -357,9 +410,16 @@ and it is why Q#HS7 blocks: §4 cannot be written until it exists. - Round-trip identity for `place_of_byte` / `byte_at_place` at non-zero offset — the Q#HS7(d) invariant, walked exhaustively over a short line rather than sampled, as Stage 3 established. -- **The Q#HS7(b)/(c′) cases, which have no oracle until it is - answered**: a wide glyph straddling the left edge; a tab whose - expansion straddles it. +- **The Q#HS7(c′) case**: a wide glyph straddling the left edge — the + trailing cell blank, and `byte_at_place` on it returning the glyph's + **start** byte. +- **The Q#HS7(c″) case**: a tab whose expansion straddles the edge, + with every surviving cell still mapping to the byte **after** the + tab. This one is a **regression** witness rather than a new claim — + `byte_at_place` already behaves this way at offset 0 + (`src/text_view.rs:224`), so the test asserts scroll did not perturb + it, and it should fail if the walk is "optimized" to start at the + effective edge instead of column 0. - **A multi-line fixture whose glyph widths DIFFER at the same column** — the case (c′) exists for, and the one a single-line sweep cannot reach. At one `view_left`, one line must take the straddle path and