docs: Stage 4 revision 4 — the tab rule was already there
Two small revisions, and every Stage 4 question is now answered. Approval is NOT recorded; no implementation may begin. Q#HS7(c″) — the tab-straddle mapping. Recorded as PRESERVED rather than chosen, because it already exists: `byte_at_place`'s doc comment says it rounds forward to the next character boundary, and the walk accumulates past the tab byte and returns on the NEXT character's start column, so every column inside a tab's expansion already yields the post-tab offset (src/text_view.rs:224, :243-254). The requirement on Stage 4 is therefore that horizontal scroll not PERTURB it — which makes its witness a regression test, and one that should fail if the walk is ever "optimized" to start at the effective edge instead of column 0. The obvious objection is that (c′) rounds backward and (c″) forward, so the framing answers it. 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 exactly when it is what 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″) the (d) contract is total over visible cells: ordinary character → its own start; bisected wide glyph → the glyph's start; tab expansion → the byte after the tab. Q#HS5 approved as stated, with both conditions written into the approval rather than attached as advice: `#[serde(default)]` and a literal v1 JSON fixture omitting the field, asserting restore at zero. The handoff said "Stage 4 is the remainder". It now says Stages 4 AND 5 remain and the arc closes at Stage 5, carries the Q#HS1 time box, and states explicitly that Rule 4 must not retire the long-lines lane at Stage 4's merge. It also records that the unreachable caveat is missing from the setting's description — the #221 gap — so that fact lives in the durable doc and not only in a lane block that will eventually be removed. Ledger: the question list is consolidated (the accepted answers had begun duplicating the blocking entries they resolved), keeping the withdrawn (c)'s reasoning because the trap generalizes to any future window-wide value derived from per-line content. 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
baa71b607e
commit
403fb7fb07
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue