From d6b5285234dae4195c7cc7f09601fd205b75c0c6 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 6 Aug 2026 22:51:03 +0200 Subject: [PATCH] docs: the GPU cannot count rows it never shaped Revision 16 gave the GPU its scroll-indicator total on the premise that "cosmic-text already knows each line's visual height". It does not. rebuild_code_slice feeds cosmic-text current_text[vstart..vend] and nothing else, because Session S1 found that feeding the whole rope made large-file editing O(file) per keystroke. The GPU's layout holds the viewport slice plus overscan; it cannot yield total visual rows, nor the cursor's or top's row ordinal. Re-shaping the whole document to recover them would reintroduce exactly the cost that design exists to prevent --- for a status-line readout. So the aggregate is abandoned rather than relocated. NN% is computed from BYTE POSITION in both frontends; truncate keeps today's visible-line percentage. No total, no cache, no invalidation. Three reasons it is abandoned rather than approximated. Rows-per-line could be computed as ceil(width / cols) without shaping, but cosmic-text decides the real break points, so the number could disagree with what is on screen --- the same approximate-parity trap Q#LL5 rejected for whitespace wrapping. Letting the TUI use row ordinals and the GPU use bytes would show two percentages for one buffer, which is this lane's own defect a third time. And All/Top/Bot are unaffected either way: they are local predicates, they stay exact, and they are the states a user actually reads. This retires the cache from revision 15 and the fold-key correction from revision 16. That correction was right for the design as it stood; the design moved under it. Section 5d.2 is marked SUPERSEDED rather than deleted, because "the key was fixed" and "there is no key" are different states and a later reader should be able to tell which one happened here. Adds the large-file guard as a witness rather than an intention: an indicator paint must leave the GPU's view_range and shaped_top untouched, must not lay out beyond the viewport in the TUI, and open_100mb_under_200ms must still pass with wrap as the default mode. Without those, the decision is an unenforced comment and a later "improvement" to a real row count would silently restore O(file) work. Known imprecision, stated rather than left to be discovered: under folds a byte percentage counts hidden bytes. Folding is TUI-only today, and it matches Emacs. It belongs to the GPU folding lane, not this one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 12 +++- docs/long-lines-framing.md | 136 ++++++++++++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 19 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 87c1249..834b793 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -306,7 +306,7 @@ records. Recover: `git fetch githubsucks && git checkout long-lines`. **This block was written with the lane's first commit, before any PR exists** — the standing correction from #171, #215 and #220. -- **Framing `docs/long-lines-framing.md` revision 16.** Q#LL1-LL7 +- **Framing `docs/long-lines-framing.md` revision 17.** Q#LL1-LL7 settled; **Q#LL8 not yet approved** --- implementation is blocked on it, because it changes what gets built rather than how. - **Independent of #220.** Stage 2 (GUI zoom) and Stage 3 share no @@ -369,6 +369,16 @@ for the user to express a preference in either. contents, which cannot be forgotten, rather than a maintained revision counter that can. + **Then the aggregate was abandoned entirely (revision 17).** The GPU + shapes **only the viewport slice** — Session S1 found the whole rope + made large-file editing `O(file)` per keystroke — so its layout + cannot yield a total, and re-shaping the document for a status-line + readout would reintroduce that cost. `NN%` is now **byte-based in + both frontends**; `All`/`Top`/`Bot` stay exact because they are local + predicates. This retires the cache *and* the fold-key fix above, + which is kept in the framing marked superseded so a reader can tell + "the key was fixed" from "there is no key". + ### The two decisions most likely to be questioned later - **GUI users lose word wrap.** Character-wrap parity is cheap and diff --git a/docs/long-lines-framing.md b/docs/long-lines-framing.md index 3ed9872..c5e664b 100644 --- a/docs/long-lines-framing.md +++ b/docs/long-lines-framing.md @@ -1,7 +1,7 @@ # Long lines — QoL Stage 3 -**Status: revision 16 — branched as `long-lines`; Q#LL1–LL7 settled; -**Q#LL8 not yet approved** (two corrections from review of `1c9ff6a`). +**Status: revision 17 — branched as `long-lines`; Q#LL1–LL7 settled; +Q#LL8 awaiting approval (aggregate abandoned for a byte percentage). Not yet implemented.** **Revision 2** corrected a load-bearing error in revision 1: it claimed @@ -225,7 +225,7 @@ forbids. Every wrap-point example is now the explicit triple same `row`, which is the information a redefinition would have destroyed. -**Revision 16 — the current one.** Review of `1c9ff6a` found two more, +**Revision 16** — review of `1c9ff6a` found two more, both in Q#LL8, and both the same shape: a fix that looked complete because it was correct in one of two places. @@ -250,6 +250,32 @@ and additive `sub_row`: self-validating over maintained. And *content* width, not window width, because the gutter changes at the line-count digit boundary. +**Revision 17 — the current one.** Review of `b95506f` falsified the +premise revision 16 gave the GPU: it said cosmic-text "already knows +each line's visual height". It does not. The GPU shapes **only the +viewport slice** — `rebuild_code_slice` feeds cosmic-text +`current_text[vstart..vend]` because Session S1 found the whole rope +made large-file editing **O(file) per keystroke** +(`pmacs-gpu/src/main.rs:7912`, `:1710`). Its layout cannot produce a +total, and re-shaping the document to get one would reintroduce exactly +that cost — for a status-line readout. + +**So the aggregate is abandoned rather than relocated (§5d.4): `NN%` is +byte-based in both frontends**, with `truncate` keeping today's +visible-line percentage. Computing rows arithmetically would only +*approximate* what cosmic-text actually renders — the same trap Q#LL5 +rejected — and letting the two frontends use different rules would be +this lane's own defect a third time. `All`/`Top`/`Bot` are unaffected: +they are local predicates and stay exact, and they are what users +actually read. + +**This makes revision 16's cache — and its fold-key correction — +unnecessary.** That correction was right for the design as it stood; +the design moved under it. §5d.2 is marked superseded rather than +deleted, so a later reader can tell "the key was fixed" from "there is +no key". §5d.5 adds the **large-file guard witnesses**, because "no +whole-document work happens" must be enforced, not merely intended. + Drafted while GitHub Actions was in a major outage and #220 could not merge. Nothing here depends on #220 landing; the two lanes touch no common code. @@ -1146,8 +1172,10 @@ with §5b.5's identity-case strategy. - `All` --- every visual row of the buffer is on screen. - `Top` --- the first visual row is on screen and `All` does not hold. - `Bot` --- the last visual row is on screen and `All` does not hold. -- `NN%` --- the cursor's **visual row ordinal** as a percent of the - buffer's total visual rows. +- `NN%` --- **byte position**, not a visual-row ordinal. See §5d.4: + a true row ordinal is unobtainable in the GPU without violating its + large-file design, and approximating it would diverge from what is + actually rendered. ### 5d.2 What must be computed, and what must not @@ -1159,6 +1187,13 @@ that already happens. **Only `NN%` needs a total**, which matters because `All`/`Top`/`Bot` are the states a user reads most and the common cases stay `O(viewport)`. +> **SUPERSEDED by §5d.4 (revision 17).** There is no total and no +> cache: `NN%` is byte-based in both frontends. Everything below was +> correct for the design as it stood in revision 16 and is kept because +> the reasoning still applies to any future aggregate --- and because a +> reader should be able to tell "the key was fixed" from "there is no +> key". Skip to §5d.4 for what is built. + The total may be computed **lazily and cached** --- but revision 15's key was wrong, and review of `1c9ff6a` caught it. It said "buffer generation, width and mode". Two corrections: @@ -1241,31 +1276,96 @@ formatter test valid, including the GPU's correctly pins line-space behavior and must **not** silently change meaning. -The GPU derives its total from its own layout, not from a wire message: -cosmic-text already knows each line's visual height, so this is a local -query there --- and Q#LL7's message tells it *which mode* to be in, not -how many rows there are. +**Revision 16 said the GPU could derive its total locally because +"cosmic-text already knows each line's visual height". That is false**, +and review of `b95506f` caught it. The GPU's cosmic-text buffer holds +**only the viewport slice**: `rebuild_code_slice` shapes +`current_text[vstart..vend]` and nothing else +(`pmacs-gpu/src/main.rs:7912`), because Session S1 found that feeding +the whole rope "made large-file editing **O(file) per keystroke**". +`scroll_top`'s own doc says the same (`:1710`). Its layout cannot yield +total visual rows, nor the cursor's or top's visual-row ordinal. + +Re-shaping the whole document to get them would reintroduce exactly the +cost Session S1 exists to prevent. That is not a tradeoff worth +reopening for a status-line readout. + +### 5d.4 The aggregate is abandoned: byte percentage, both frontends + +**Decision: under `wrap`, `NN%` is computed from BYTE POSITION, in both +frontends. No aggregate, no cache, no invalidation.** Under `truncate`, +both keep today's visible-line percentage unchanged. + +This is the fallback §5d.2 named as a contingency, promoted to the +plan. The reasoning: + +- **The GPU cannot produce a true total** without violating Session S1. +- **Arithmetic would only approximate it.** Rows-per-line could be + computed as `ceil(width / cols)` without shaping --- but cosmic-text + decides the real break points, so the number could disagree with what + is actually on screen. That is the same *approximate parity* trap + Q#LL5 rejected for whitespace wrapping, and it should be rejected + here for the same reason. +- **A divergent choice would be worse than either.** Visual-row `NN%` + in the TUI and byte `NN%` in the GPU means the same buffer shows two + different percentages --- this lane's own defect, for a third time + (§5d.3). One rule in both frontends is the point. +- **`All`/`Top`/`Bot` are unaffected and stay exact**, because they are + local predicates (§5d.2). Those are the states a user actually reads; + `NN%` is a coarse readout, and a byte-based one is honest rather than + wrong. +- Emacs computes its percentage from buffer position too. + +**This makes §5d.2's cache unnecessary, including the fold-key +correction from revision 16.** That correction was right for the design +as it then stood, and the design has since changed underneath it --- +recorded rather than quietly deleted, because "we fixed the key" and +"there is no key" are different states and a later reader should be +able to tell which happened. + +**Known imprecision, stated rather than discovered:** under folds, a +byte percentage counts hidden bytes. Folding is TUI-only today (the GPU +fold stage is unstarted), so this is currently a single-frontend +nuance, and it matches Emacs. It should be revisited **by the GPU +folding lane**, not by this one. + +### 5d.5 The large-file guard + +Because the whole point is that no whole-document work happens, that +must be a **witness, not an intention**: + +- Painting the indicator on a large buffer with `wrap` active must + perform **no whole-document layout**. In the GPU this is observable + directly --- `view_range` and `shaped_top` must be unchanged by an + indicator paint --- and in the TUI by bounding the lines laid out to + the viewport. +- The existing `open_100mb_under_200ms` gate (M1) must still pass with + `wrap` as the default mode, which is the end-to-end version of the + same claim. + +Without these, the byte-percentage decision is an unenforced comment, +and a later "improvement" to a real row count would silently reintroduce +`O(file)` work. **The duplication is itself the hazard worth naming.** Two copies means two call sites must change, and nothing in the type system connects them. That is the same shape as Q#LL7's three resend triggers: a correct fix in one place that looks complete. -### 5d.4 Verification +### 5d.6 Verification - **The reported case, as a direct witness, IN BOTH FRONTENDS:** one source line, viewport shorter than its wrapped height, mode `wrap` --- the indicator must **not** be `All`. This fails against revision 14's design in the TUI and revision 15's in the GPU, which is what makes it worth writing first, twice. -- **A "cache, then toggle a fold" witness.** Paint `NN%` so the total - is cached, collapse (or expand) a fold **without any edit, resize or - mode change**, and assert the indicator changes. This fails against - revision 15's cache key, which is the point. -- **A digit-boundary witness**, since content width is in the key: a - buffer crossing 9 -> 10 or 99 -> 100 lines changes the gutter and - therefore the wrap width, and the total must not be served stale - across it. +- **The large-file guards of §5d.5**, in both frontends --- an + indicator paint must leave the GPU's `view_range` / `shaped_top` + untouched, and must not lay out beyond the viewport in the TUI. +- **`open_100mb_under_200ms` (M1) with `wrap` as the default mode.** +- The fold witnesses revision 16 asked for are **withdrawn with the + cache they guarded** (§5d.2, §5d.4). What survives from that round is + the `truncate` control below, which still pins the identity case. - `Top` at the buffer start, `Bot` at the end, `All` only when every visual row fits --- each with a wrapped line present. - **A `truncate` control** asserting the indicator is byte-identical to