docs: Stage 4 revision 3 — a setter-time snap cannot exist
P1 — Q#HS7(c) WITHDRAWN, and my vote was wrong rather than vague. Revision 2 voted to snap `view_left` to a valid boundary at the moment it is 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 line 3 and ordinary ASCII on line 4. No setter-time value is canonical for every visible line. Snapping per line instead is worse — the same source column would appear at different screen columns on different rows, destroying the alignment a column-oriented view exists to provide. Replaced by (c′), a per-line effective edge. `view_left` is stored unsnapped; each line derives its own edge during the walk it already performs from column 0. Where the requested edge bisects a wide glyph on THAT line, the finding's actual question — what occupies the leftmost cell — is answered: it paints as a space carrying the glyph's style, and the mapping DESIGNATES that cell to the wide glyph's start byte. That keeps `byte_at_place` total over visible cells, preserves the round trip (`place_of_byte(start)` reports the straddle and designates cell 0), and gives a click there the character a user would expect. Bytes lying entirely left of the edge are reported not-visible rather than clamped to column 0, because clamping would make arbitrarily many bytes share cell 0 and destroy (d). Recorded as deliberately NOT the mirror of Stage 3's right-edge rule: under wrap a too-wide glyph is pushed to the next row entirely, and at the left edge under truncate there is no next row, so the same intent requires a different rule. Stated so nobody "fixes" one to match the other. (d) is amended accordingly: the invariant is a property of `(view_left, line)`, not of `view_left` alone — which is what makes a multi-line fixture with differing glyph widths at the same column the DISCRIMINATING test rather than an extra one. A single-line sweep passes against the withdrawn design. P1 — Stage 4 no longer closes the arc, and the stale claim was load-bearing in the wrong direction. Rule 4 removes a lane when its ARC is done, so a framing asserting Stage 4 closes it would license retiring the lane at the TUI merge — orphaning the very Stage 5 that Q#HS1's time box exists to guarantee, while `truncate` is still a dead end in the GUI. Both the framing opening and the lane header now say the arc closes at Stage 5, and the lane carries an explicit "Rule 4 does not apply at Stage 4's merge". P2 — the stale Stage 3 residue in the ledger claimed the unreachable caveat is in the setting description, contradicting revision 2 forty lines above it. Corrected in place: the caveat lives only in the toggle's status message and a source comment, and a user who sets the mode in init.lua is told nothing. Q#HS5 now states the concrete condition rather than an instruction to check one. 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`. "Yes, no version bump" is sound only with the annotation AND a regression fixture holding literal v1 JSON without the field. The reverse direction already works — an old binary meets an unknown field, which serde ignores absent deny_unknown_fields, and there is none in that file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
5d041a4f17
commit
baa71b607e
|
|
@ -435,7 +435,7 @@ Whether `docs/ci-red-signatures.md` should grow a short non-row section
|
|||
for this class is an open question for its owner, not something this
|
||||
lane decided.
|
||||
|
||||
## Long lines (QoL arc) — Stage 3 MERGED as #221; Stage 4 is framing
|
||||
## Long lines (QoL arc) — Stage 3 MERGED as #221; Stages 4 AND 5 ahead
|
||||
|
||||
**Rewritten, not removed.** Rule 4 removes a lane when its ARC is done;
|
||||
this one has Stage 4 ahead. Stage 3's durable facts are absorbed into
|
||||
|
|
@ -443,6 +443,14 @@ this one has Stage 4 ahead. Stage 3's durable facts are absorbed into
|
|||
than deferred — so what remains here is the Stage 4 plan and only the
|
||||
Stage 3 residue that constrains it.
|
||||
|
||||
> **RULE 4 DOES NOT APPLY AT STAGE 4's MERGE.** The arc closes at
|
||||
> **Stage 5** (GPU horizontal scroll), not Stage 4. Q#HS1 split the GPU
|
||||
> out deliberately and time-boxed it; retiring this lane when the TUI
|
||||
> half merges would orphan exactly the half the time box exists to
|
||||
> guarantee, and would do so while `truncate` is still a dead end in
|
||||
> the GUI. **Do not remove this block until Stage 5 has merged**, and
|
||||
> read Q#HS1's four time-box items before concluding otherwise.
|
||||
|
||||
**Branch `horizontal-scroll`**, based on `githubsucks/main` @ `02f3ec3`
|
||||
(the #221 merge). `githubsucks/horizontal-scroll` is the authoritative
|
||||
tip — the ref, not a SHA, since any edit to this block advances past
|
||||
|
|
@ -450,7 +458,7 @@ whatever SHA it records. Recover:
|
|||
`git fetch githubsucks && git checkout horizontal-scroll`.
|
||||
|
||||
**Status: framing, NOT approved.** `docs/horizontal-scroll-framing.md`
|
||||
revision 2. No implementation may begin.
|
||||
revision 3. No implementation may begin.
|
||||
|
||||
**Answered by the user 2026-08-07:**
|
||||
|
||||
|
|
@ -472,17 +480,36 @@ revision 2. No implementation may begin.
|
|||
|
||||
- **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`. Four coupled parts:
|
||||
the unit; which columns may be a left edge; the snap rule for an
|
||||
invalid one; and the invariant rendering and coordinate mapping
|
||||
share. Without the last, **a painter that clips where the mapper does
|
||||
not puts clicks on the wrong character**, silently, only on lines
|
||||
wide enough to scroll. §4's sketch has no oracle until this is
|
||||
answered.
|
||||
- **Q#HS5 — does `view_left` survive a restart?** Vote: yes, defaulted,
|
||||
no `DESKTOP_VERSION` bump — *after* confirming `SavedLeaf`'s
|
||||
deserializer tolerates a missing field, which is a claim about serde
|
||||
and not about the struct shape §1.4 cites.
|
||||
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.
|
||||
|
||||
|
|
@ -506,9 +533,13 @@ amending the description is now a Stage 4 deliverable (framing §6).
|
|||
this and it is survivable — but Stage 3 signed up for it as *a
|
||||
decision*, and Stage 4 is where the bill arrives.
|
||||
- **`truncate` is the mode Stage 4 makes navigable.** Today text past
|
||||
the right edge is not merely off-screen but **unreachable**; that is
|
||||
stated in the setting's description and in `ui.toggle-line-wrap`'s
|
||||
status message, both of which should be revisited when scroll lands.
|
||||
the right edge is not merely off-screen but **unreachable** — and
|
||||
that is stated **only** in `ui.toggle-line-wrap`'s status message and
|
||||
a source comment, **not** in the setting's description
|
||||
(`builtin/runtime/linewrap.lua:23` says just "truncate at the edge").
|
||||
A user who sets the mode in `init.lua` and never invokes the toggle
|
||||
is told nothing. Amending the description is a Stage 4 deliverable
|
||||
(framing §6); the status message is revisited when scroll lands.
|
||||
- **Under `wrap`, horizontal scroll is meaningless.** Stage 4's surface
|
||||
is therefore conditional on the mode, which is a coherence question
|
||||
(one concept, two behaviors) and not only an implementation one.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
# Horizontal scroll — QoL Stage 4
|
||||
|
||||
**Status: revision 2 — NOT APPROVED. Q#HS1, HS2 and HS6 answered by the
|
||||
user (2026-08-07); Q#HS3–HS5 stand; Q#HS7 is NEW and BLOCKING. No
|
||||
**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.**
|
||||
|
||||
This closes the QoL arc opened by one daily-driver report. Stage 1
|
||||
(#219) made the TUI survive terminal zoom; Stage 2 (#220) gave the GUI
|
||||
native zoom; Stage 3 (#221) added `ui.line-wrap` and made `wrap` the
|
||||
default. Stage 4 is the other half of the user's own sentence:
|
||||
**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.
|
||||
The claim is not merely stale — it is load-bearing in the wrong
|
||||
direction: `docs/active-work.md`'s **Rule 4 removes a lane when its
|
||||
ARC is done**, so a framing asserting Stage 4 closes the arc would
|
||||
license retiring this lane at the TUI merge, **orphaning the very
|
||||
Stage 5 that Q#HS1's time box exists to guarantee**. The arc closes at
|
||||
**Stage 5**.
|
||||
|
||||
Stage 1 (#219) made the TUI survive terminal zoom; Stage 2 (#220) gave
|
||||
the GUI native zoom; Stage 3 (#221) added `ui.line-wrap` and made
|
||||
`wrap` the default. Stage 4 is the TUI half of the other half of the
|
||||
user's own sentence:
|
||||
|
||||
> long lines need to either **wrap somehow or be scrollable**. […] This
|
||||
> should also be something that the user can configure, whether to wrap
|
||||
|
|
@ -181,15 +192,36 @@ 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**
|
||||
### Q#HS5 — does `view_left` survive a restart? **OPEN, with the condition now concrete**
|
||||
|
||||
`view_top` does (§1.4). Consistency argues yes; a defaulted field
|
||||
avoids a `DESKTOP_VERSION` bump.
|
||||
`view_top` does (§1.4). Consistency argues yes.
|
||||
|
||||
**My vote: yes, defaulted, no version bump** — but **confirm
|
||||
`SavedLeaf`'s deserializer tolerates a missing field before relying on
|
||||
it.** §1.4 cites the struct's *shape*, not serde's behavior on it, and
|
||||
that gap is exactly the kind §1.3 exists to warn about.
|
||||
**Verified, not assumed:** `SavedLeaf` is a plain
|
||||
`#[derive(Serialize, Deserialize)]` (`src/desktop.rs:85`) with **no
|
||||
`#[serde(default)]` on any field and none anywhere in the file**. So
|
||||
serde will **reject** a version-1 desktop JSON that omits a newly added
|
||||
`view_left` — a missing field is a deserialization error, not a zero.
|
||||
Revision 2 cited the struct's shape as though it settled serde's
|
||||
behavior on it; it did not.
|
||||
|
||||
**"Yes, persisted, no `DESKTOP_VERSION` bump" is sound only with both
|
||||
of:**
|
||||
|
||||
1. **`#[serde(default)]` on the new field.** This is the whole of the
|
||||
new-binary-reads-old-file direction.
|
||||
2. **A regression fixture**: a literal version-1 desktop JSON with no
|
||||
`view_left`, deserialized in a test, asserting it restores at offset
|
||||
0 rather than erroring. Without this, (1) is an untested claim about
|
||||
a crate's behavior — which is precisely the failure this question
|
||||
was reopened for.
|
||||
|
||||
**The other direction already works, and that is why no bump is
|
||||
needed.** An old binary reading a new file passes the version check
|
||||
(`version` is still 1, `src/desktop.rs:366`) and then meets an unknown
|
||||
`view_left` field — which serde **ignores** by default, and
|
||||
`src/desktop.rs` sets no `deny_unknown_fields` anywhere (verified). So
|
||||
both directions are safe at `DESKTOP_VERSION = 1` **given (1)**, and
|
||||
neither is safe without it.
|
||||
|
||||
### Q#HS6 — the coherence statement **ANSWERED: keep `wrap` default**
|
||||
|
||||
|
|
@ -231,32 +263,83 @@ nothing and lose tab correctness. Starting at 0 and suppressing paint
|
|||
until `col >= view_left` preserves tab stops **for free**, and costs no
|
||||
more than `paint_line` already pays under wrapping.
|
||||
|
||||
**(b) Which columns may be a left edge.** A tab straddling the edge is
|
||||
unambiguous: its expansion is width-1 spaces, so the remaining ones
|
||||
paint. **A wide (width-2) glyph is not** — a grid cannot paint half of
|
||||
one.
|
||||
**(b) What happens at a left edge that bisects a glyph.** A tab
|
||||
straddling the edge is unambiguous: its expansion is width-1 spaces, so
|
||||
the remaining ones paint. **A wide (width-2) glyph is not** — a grid
|
||||
cannot paint half of one.
|
||||
|
||||
*My vote: a left edge may not fall inside a wide glyph's cells.*
|
||||
**(c) ~~The snap rule when an invalid edge is requested.~~ WITHDRAWN
|
||||
(revision 3).**
|
||||
|
||||
**(c) The snap rule when an invalid edge is requested.** Stage 3's
|
||||
coordinate contract is *"identity on canonical inputs; otherwise
|
||||
projection to the contract's **designated** canonical representative"* —
|
||||
designated, not nearest, because the direction differs per function and
|
||||
"nearest" hides that.
|
||||
> Revision 2 voted *"a left edge may not fall inside a wide glyph"* plus
|
||||
> *"snap toward the line start, at the moment `view_left` is set"*.
|
||||
> **That cannot hold, and the reason is structural rather than a detail
|
||||
> to tune.**
|
||||
>
|
||||
> `view_left` is **one** per-window display column. "Does column N
|
||||
> bisect a wide glyph?" is a **per-line** question: column 11 can be a
|
||||
> wide glyph's trailing cell on line 3 and an ordinary ASCII cell on
|
||||
> line 4. **No single setter-time value is canonical for every visible
|
||||
> line**, so a snap performed once is simply wrong for most of them —
|
||||
> and the invariant in (d), which the whole question exists to serve,
|
||||
> would stay undefined exactly where it matters.
|
||||
>
|
||||
> Snapping *per line* is the other way to read it, and it is worse: the
|
||||
> same source column would then appear at different screen columns on
|
||||
> different rows, destroying the vertical alignment that a
|
||||
> column-oriented view exists to provide.
|
||||
|
||||
*My vote: snap toward the line start.* Snapping left can only reveal a
|
||||
character, never hide one that was visible; snapping right can hide the
|
||||
very glyph the user scrolled to reach. And snapping at the moment
|
||||
`view_left` is **set** — rather than clipping in the painter — keeps
|
||||
one canonical value that both painter and mapper read, instead of two
|
||||
that can disagree.
|
||||
**(c′) The per-line effective edge, which replaces it.**
|
||||
|
||||
`view_left` is stored **unsnapped** — the requested display column,
|
||||
constrained only to `>= 0` and whatever maximum the design picks. Each
|
||||
line derives its own **effective edge** during the walk it already
|
||||
performs from column 0.
|
||||
|
||||
When the requested edge bisects a wide glyph *on this line*, that
|
||||
glyph's trailing cell is the leftmost visible cell. It cannot be
|
||||
painted as half a glyph, so:
|
||||
|
||||
- **It paints as a space**, carrying the glyph's own cell style.
|
||||
- **The mapping designates that cell to the wide glyph's START byte.**
|
||||
|
||||
Both halves are load-bearing, and the second is the part the finding
|
||||
correctly says was missing:
|
||||
|
||||
- The cell visually belongs to that character, so a click there
|
||||
selecting it is what a user expects.
|
||||
- It keeps `byte_at_place` **total** over visible cells — every painted
|
||||
cell maps to some byte, with no hole at column 0.
|
||||
- It preserves the round trip: `place_of_byte(glyph_start)` reports the
|
||||
straddle and designates cell 0, so `byte_at_place(0) == glyph_start`.
|
||||
|
||||
**And the direction rule `place_of_byte` needs at the left edge:** a
|
||||
byte whose cells lie *entirely* left of the effective edge is **not
|
||||
visible**, and `place_of_byte` must report that rather than clamping to
|
||||
column 0. Clamping would make arbitrarily many bytes share cell 0 and
|
||||
destroy (d). Only the straddling glyph designates cell 0.
|
||||
|
||||
**This is deliberately NOT the mirror of Stage 3's right-edge rule**,
|
||||
and the asymmetry should be stated so nobody "fixes" one to match the
|
||||
other. Under `wrap`, a wide glyph that will not fit at the right edge is
|
||||
pushed to the next row **entirely** (`advance_wrapped`, with its
|
||||
`max_cols >= 2` guard). At the left edge under `truncate` there is no
|
||||
next row to push to, so the blank-plus-designation rule is what the
|
||||
same intent requires here.
|
||||
|
||||
**(d) The invariant rendering and coordinate mapping share.** For a
|
||||
given `view_left`, `byte_at_place` must invert `place_of_byte` on every
|
||||
canonical input, and `paint_line` must place exactly the bytes
|
||||
`place_of_byte` claims. If the painter clips where the mapper does not,
|
||||
**clicks land on the wrong character** — silently, and only for lines
|
||||
wide enough to scroll.
|
||||
given `view_left` **and line**, `byte_at_place` must invert
|
||||
`place_of_byte` on every canonical input, and `paint_line` must place
|
||||
exactly the bytes `place_of_byte` claims. If the painter clips where the
|
||||
mapper does not, **clicks land on the wrong character** — silently, and
|
||||
only for lines wide enough to scroll.
|
||||
|
||||
**"And line" is what (c′) forced**, and it is the whole of that
|
||||
finding: the invariant is not a property of `view_left` alone. It is a
|
||||
property of `(view_left, line)`, because the effective edge is derived
|
||||
per line. A test that fixes one line and sweeps offsets will not see
|
||||
the failure; the oracle has to sweep **lines whose glyph widths differ
|
||||
at the same column**.
|
||||
|
||||
*This is the invariant the verification sketch needs as its oracle*,
|
||||
and it is why Q#HS7 blocks: §4 cannot be written until it exists.
|
||||
|
|
@ -274,9 +357,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
|
||||
- **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; a snap request landing inside each.
|
||||
expansion straddles it.
|
||||
- **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
|
||||
another the ordinary path, with the (d) invariant holding on both.
|
||||
A test that fixes one line and sweeps offsets passes against the
|
||||
withdrawn setter-time snap, which is what makes this the
|
||||
discriminating fixture rather than an extra one.
|
||||
- **A PTY acceptance test for reachability**, following
|
||||
`tests/long_line_readable_acceptance.rs`. That file's `truncate`
|
||||
control currently asserts the tail is **absent** — Stage 4 must
|
||||
|
|
|
|||
Loading…
Reference in New Issue