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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-06 22:51:03 +02:00
parent b95506f6ca
commit d6b5285234
No known key found for this signature in database
2 changed files with 129 additions and 19 deletions

View File

@ -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

View File

@ -1,7 +1,7 @@
# Long lines — QoL Stage 3
**Status: revision 16 — branched as `long-lines`; Q#LL1LL7 settled;
**Q#LL8 not yet approved** (two corrections from review of `1c9ff6a`).
**Status: revision 17 — branched as `long-lines`; Q#LL1LL7 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