docs: Stage 4 revision 2 — three answers, and the contract revision 1 forgot
P1 — THE PROTOCOL-BUMP GATE WEAKENED THE SWEEP IT MEANT TO STRENGTHEN. Revision 1 said `--tests --no-fail-fast` REPLACES the `--workspace` line. Measured: `--tests` selects 108 targets, `--workspace` selects 110, and the two it drops are `pmacs_protocol` and `pmacs_gpu`. On a PROTOCOL bump, dropping the protocol crate's tests is the wrong loss — and it also silently dropped `--skip basedpyright`. Worse, this lane's own remediation sweep used `--tests`, so it never ran pmacs-protocol's 25 tests, including the `scroll::classify` tests this lane had just written. They pass (verified 25/0), but by luck. A correction that reproduces the shape of the mistake it corrects is worth naming, so §3 and §5 both say `--workspace`, additive, with `-- --skip basedpyright` retained in both feature configurations. P1 — NO COORDINATE CONTRACT FOR view_left. Revision 1 decided what moves the viewport and never said what its offset IS — the same omission as shipping WrapMode with no DisplayCoord. Its verification sketch named tabs and wide characters with no oracle for either, because nothing defined what a left edge is. Q#HS7 is new and BLOCKING, in 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. Votes recorded — display column (tab stops come free, since the walk must start at column 0 either way and a byte offset buys nothing); a left edge may not fall inside a wide glyph; snap toward the line start (snapping left can only reveal a character, snapping right can hide the one the user scrolled to reach); and snap when the value is SET, not in the painter, so one canonical value serves both readers. Part (d) is why it blocks: if the painter clips where the mapper does not, clicks land on the wrong character — silently, and only on lines wide enough to scroll. P2 — THE CAVEAT IS NOT IN THE SETTING DESCRIPTION. Revision 1 said it was. builtin/runtime/linewrap.lua:23 says only "truncate at the edge"; "unreachable" lives in the toggle's status message and a source comment, neither of which a user sees who sets the mode in init.lua. That is a real, small user-facing gap shipped in #221. Claim corrected, and amending the description is now a Stage 4 deliverable (§6) with the text depending on which stage has landed. THE THREE ANSWERS, recorded with the reasoning that decided them: HS1 — GPU is Stage 5. The distinction that matters is that Stage 3's defect was never "the frontends differ" but "the frontends differ and nobody chose that". Time box made concrete per the request: Stage 5 is the immediately-next QoL lane, `wrap` stays default until it lands, release notes state the asymmetry, and the truncate affordances name the GUI gap meanwhile. HS2 — automatic only. No command surface; the cursor-visibility pass gains a horizontal component. HS6 — `wrap` stays default, and the reason given is stronger than the one revision 1 reasoned from. I had framed it as "if scroll makes truncate good, reconsider the default". With the GPU deferred, a truncate default would ship a mode navigable in the TUI and a dead end in the GUI for every user who never opened the setting. HS1 and HS6 are coupled: the split is only safe because the default does not move. Q#HS4 is deferred rather than closed — not live under automatic-only, but the snap-back hazard is real and rediscovering it costs more than carrying the paragraph. Q#HS5 stands, with the caveat that §1.4 cites the struct shape and not serde's behavior on a missing field. 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
b110684a62
commit
5d041a4f17
|
|
@ -449,9 +449,50 @@ 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 1. No implementation may begin until its questions are
|
||||
answered.
|
||||
**Status: framing, NOT approved.** `docs/horizontal-scroll-framing.md`
|
||||
revision 2. No implementation may begin.
|
||||
|
||||
**Answered by the user 2026-08-07:**
|
||||
|
||||
- **Q#HS1 — the GPU is Stage 5, not Stage 4.** A conscious, bounded
|
||||
divergence rather than a repeat of Stage 3's accidental one. The time
|
||||
box is concrete: Stage 5 is the *immediately-next* QoL lane after
|
||||
Stage 4 merges, `wrap` stays the default until it lands, Stage 4's
|
||||
release notes state the asymmetry, and the `truncate` affordances
|
||||
name the GUI gap while it exists.
|
||||
- **Q#HS2 — automatic only.** The cursor-visibility pass gains a
|
||||
horizontal component; no commands, no bindings, no new interaction
|
||||
island. Explicit `ui.scroll-*` is deliberately out.
|
||||
- **Q#HS6 — `wrap` stays the default.** Coupled to Q#HS1: with the GPU
|
||||
deferred, a `truncate` default would ship a mode that is navigable in
|
||||
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 (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.
|
||||
- **Q#HS3** is re-confirmed rather than open (per-window, per Q#LL2);
|
||||
**Q#HS4** is deferred, live only if explicit commands arrive.
|
||||
|
||||
**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
|
||||
"truncate at the edge"; the word appears in the toggle's status message
|
||||
and a source comment, neither of which a user sees if they set the mode
|
||||
in `init.lua`. **A real, small user-facing gap shipped in #221**;
|
||||
amending the description is now a Stage 4 deliverable (framing §6).
|
||||
|
||||
### What Stage 3 shipped that Stage 4 must live with
|
||||
|
||||
|
|
@ -512,10 +553,21 @@ answered.
|
|||
|
||||
Stage 3 put eight broken version assertions on CI by running
|
||||
`CLAUDE.md`'s short gate list instead of `docs/agent-handoff.md` §3's,
|
||||
which includes a full sweep. **§3 is the authority.** If Stage 4 touches
|
||||
`PROTOCOL_VERSION` — an open question, since the GPU may need a wire —
|
||||
the sweep must be `cargo test --tests --no-fail-fast` in **both**
|
||||
feature configurations. See §3 and §5's protocol-bump bullet.
|
||||
which includes a full sweep. **§3 is the authority.**
|
||||
|
||||
**Stage 4 should not need a protocol bump at all** — Q#HS1 puts the GPU
|
||||
in Stage 5, and `view_left` is per-window TUI state with no wire. If
|
||||
that changes, §3's protocol-bump form is
|
||||
`cargo test --workspace --no-fail-fast -- --skip basedpyright` in
|
||||
**both** feature configurations.
|
||||
|
||||
**`--workspace`, not `--tests`**, and the distinction is not cosmetic:
|
||||
`--tests` selects 108 targets where `--workspace` selects 110, and the
|
||||
two it drops are **`pmacs_protocol` and `pmacs_gpu`**. Stage 3's own
|
||||
*remediation* sweep used `--tests`, so it never ran `pmacs-protocol`'s
|
||||
25 tests — including the `scroll::classify` tests that lane had just
|
||||
written. They passed, but by luck, and a correction that reproduces the
|
||||
shape of its own mistake is worth naming.
|
||||
|
||||
### Not in scope
|
||||
|
||||
|
|
|
|||
|
|
@ -467,11 +467,14 @@ someone forgot.
|
|||
why this bullet exists rather than just a pointer. Plain
|
||||
`--workspace` stops at the first failing target and builds one
|
||||
feature configuration, so it would have shown one or two of the
|
||||
eight. **On any protocol bump run `cargo test --tests --no-fail-fast`
|
||||
in BOTH configurations** — `--no-fail-fast` for the stop-at-first
|
||||
behavior, `--features crdt` because three of the eight were
|
||||
crdt-gated real-daemon tests asserting on a live socket's negotiated
|
||||
version, which is the blindness the bullet below already names.
|
||||
eight. §3 now carries the strengthened form: **`--workspace
|
||||
--no-fail-fast -- --skip basedpyright` in BOTH configurations.**
|
||||
|
||||
**Note `--workspace`, not `--tests`** — the lane's own remediation
|
||||
sweep used `--tests`, which silently drops the `pmacs_protocol` and
|
||||
`pmacs_gpu` targets. On a *protocol* bump that omits the protocol
|
||||
crate's tests, which is how a correction can reproduce the shape of
|
||||
the mistake it is correcting.
|
||||
|
||||
**Sort the failures before fixing them.** A *tripwire*
|
||||
(`assert_eq!(PROTOCOL_VERSION, N)`) is meant to fire and takes a
|
||||
|
|
@ -2124,17 +2127,33 @@ this one continues. Long-lines Stage 3 followed the short form and put
|
|||
eight broken version assertions on CI. When the two disagree, **this
|
||||
list wins**.
|
||||
|
||||
**Touching `PROTOCOL_VERSION` replaces the sweep line with:**
|
||||
**Touching `PROTOCOL_VERSION` STRENGTHENS the sweep line. It does not
|
||||
replace it:**
|
||||
|
||||
```
|
||||
cargo test --tests --no-fail-fast
|
||||
cargo test --tests --features crdt --no-fail-fast
|
||||
cargo test --workspace --no-fail-fast -- --skip basedpyright
|
||||
cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright
|
||||
```
|
||||
|
||||
Plain `--workspace` stops at the first failing target and builds one
|
||||
feature configuration, so on a version bump it reports one or two of
|
||||
what may be many. See §5's protocol-bump bullet for how to sort the
|
||||
failures it finds — some are tripwires doing their job.
|
||||
Every part is load-bearing:
|
||||
|
||||
- **`--workspace`, never `--tests`.** `--tests` selects 108 targets
|
||||
where `--workspace` selects 110, and the two it drops are
|
||||
**`pmacs_protocol` and `pmacs_gpu`**. On a protocol bump, dropping
|
||||
the protocol crate's own unit tests is precisely the wrong loss.
|
||||
Long-lines Stage 3 swept with `--tests` and so never ran
|
||||
`pmacs-protocol`'s 25 tests — including the `scroll::classify` tests
|
||||
that same lane had just written. They passed, but by luck.
|
||||
- **`--no-fail-fast`**, because cargo stops at the first failing
|
||||
target: without it a bump that breaks eight assertions reports one.
|
||||
- **`--features crdt`**, because crdt-gated real-daemon tests assert on
|
||||
a live socket's negotiated version and are invisible otherwise.
|
||||
- **`-- --skip basedpyright`** for the same reason the line above
|
||||
carries it.
|
||||
|
||||
See §5's protocol-bump bullet for how to sort the failures this finds —
|
||||
some are tripwires doing their job, and one pin
|
||||
(`ADVERTISED_PROTOCOL_VERSION`) must never be edited at all.
|
||||
|
||||
Machine-specific caveats — re-verify on a machine you haven't used
|
||||
before trusting them:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# Horizontal scroll — QoL Stage 4
|
||||
|
||||
**Status: revision 1 — NOT APPROVED. Six questions open (Q#HS1–HS6).
|
||||
No implementation may begin.**
|
||||
**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
|
||||
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
|
||||
|
|
@ -12,11 +13,20 @@ default. Stage 4 is the other half of the user's own sentence:
|
|||
> should also be something that the user can configure, whether to wrap
|
||||
> or scrollable.
|
||||
|
||||
Stage 3 shipped the *mode*. It did not ship the *navigation*, and said
|
||||
so: under `truncate`, text past the right edge is not merely off-screen
|
||||
but **unreachable**. That is recorded in the setting's description and
|
||||
in `ui.toggle-line-wrap`'s status message. Stage 4 removes that caveat
|
||||
or the caveat stands permanently.
|
||||
Stage 3 shipped the *mode*. It did not ship the *navigation*: under
|
||||
`truncate`, text past the right edge is not merely off-screen but
|
||||
**unreachable**.
|
||||
|
||||
**Revision 1 claimed that caveat "is recorded in the setting's
|
||||
description". It is not.** `builtin/runtime/linewrap.lua:23` says only
|
||||
*"How a line wider than the window is shown: wrap onto following rows,
|
||||
or truncate at the edge."* The word "unreachable" appears in
|
||||
`ui.toggle-line-wrap`'s status message and in a source comment — neither
|
||||
of which a user sees if they set `ui.line-wrap = "truncate"` in
|
||||
`init.lua` and never invoke the toggle. **That is a real, if small,
|
||||
user-facing gap shipped in #221**, and §6 makes amending the
|
||||
description a Stage 4 deliverable rather than leaving the false claim
|
||||
standing.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -29,28 +39,26 @@ same `CellGrid`; every claim below carries a citation for that reason.
|
|||
### 1.1 There is no horizontal scroll anywhere
|
||||
|
||||
No `view_left`, `scroll_left`, or `hscroll` in `src/` or `builtin/`.
|
||||
This is greenfield. The window carries `view_top`, `cursor`, and
|
||||
`goal_col` (`src/window.rs:374-376`) and nothing horizontal.
|
||||
|
||||
That matters for estimating: this is not "extend the vertical
|
||||
mechanism sideways". There is no shared abstraction to extend.
|
||||
The window carries `view_top`, `cursor`, and `goal_col`
|
||||
(`src/window.rs:374-376`) and nothing horizontal. This is greenfield —
|
||||
not "extend the vertical mechanism sideways", because there is no
|
||||
shared abstraction to extend.
|
||||
|
||||
### 1.2 The grid walk starts every line at column 0
|
||||
|
||||
`paint_line` (`src/text_view.rs:266`) walks from the line's first
|
||||
character with no offset parameter, exactly as it did before Stage 3 —
|
||||
character with no offset parameter, exactly as before Stage 3 —
|
||||
wrapping changed *where rows break*, not *where the walk starts*.
|
||||
`place_of_byte` and `byte_at_place` have the same shape.
|
||||
|
||||
So `view_left` enters the same functions Stage 3 just rewrote. **The
|
||||
wrap rule must stay written exactly once** (`advance_wrapped`,
|
||||
`src/text_view.rs:396`); a second copy differing by an offset is the
|
||||
So `view_left` enters the same functions Stage 3 just rewrote, and
|
||||
**the wrap rule must stay written exactly once** (`advance_wrapped`,
|
||||
`src/text_view.rs:396`). A second copy differing by an offset is the
|
||||
defect Stage 3 spent its review budget avoiding.
|
||||
|
||||
### 1.3 The GPU cannot use cosmic-text's horizontal scroll
|
||||
|
||||
**This is the finding most likely to invert the cost estimate, and it
|
||||
is the Stage 4 analog of revision 1's error — so it leads.**
|
||||
**The finding most likely to invert the cost estimate, so it leads.**
|
||||
|
||||
`Scroll::horizontal` is discarded throughout the GPU, and not by
|
||||
oversight: **glyphon 0.11 never applies it when placing glyphs.**
|
||||
|
|
@ -58,191 +66,260 @@ Documented at `pmacs-gpu/src/main.rs:1611`, `:6316`, `:8020`, and
|
|||
*asserted* by tests at `:16266` (`"horizontal is discarded"`), `:16337`,
|
||||
`:16737`.
|
||||
|
||||
So the GPU's half of Stage 4 cannot be "set the scroll and reshape". It
|
||||
needs a different mechanism — a shifted text origin at paint time,
|
||||
adjusted clip bounds, or something else — and that mechanism has to
|
||||
interact correctly with the gutter, the caret (`code_byte_px`),
|
||||
decoration geometry (`push_glyph_extent_rects`), and hit testing
|
||||
(`gutter_aware_rel_x`), each of which currently assumes x starts at
|
||||
`text_left()`.
|
||||
The GPU's half therefore cannot be "set the scroll and reshape". It
|
||||
needs a mechanism that does not exist — a shifted text origin at paint
|
||||
time, adjusted clip bounds, or something else — interacting correctly
|
||||
with the gutter, the caret (`code_byte_px`), decoration geometry
|
||||
(`push_glyph_extent_rects`), and hit testing (`gutter_aware_rel_x`),
|
||||
each of which assumes x starts at `text_left()`.
|
||||
|
||||
**Q#HS1 asks whether the GPU is in scope for Stage 4 at all.**
|
||||
**Answered in Q#HS1: the GPU is Stage 5.**
|
||||
|
||||
### 1.4 `view_top` is persisted; a `view_left` would want to be
|
||||
|
||||
`SavedLeaf` carries `path`, `cursor`, and `view_top` at
|
||||
`DESKTOP_VERSION = 1` (`src/desktop.rs:33`, `:276-280`). The restore
|
||||
path clamps `view_top` against the line count (`:512`).
|
||||
|
||||
A horizontal offset that does not survive restart is defensible; one
|
||||
that does needs a defaulted field or a version bump. **Q#HS5.**
|
||||
`DESKTOP_VERSION = 1` (`src/desktop.rs:33`, `:276-280`); the restore
|
||||
path clamps `view_top` against the line count (`:512`). **Q#HS5.**
|
||||
|
||||
### 1.5 The cursor-follow hazard is already documented
|
||||
|
||||
`scroll_window` (`src/editor.rs:3628`) carries the cursor with a
|
||||
vertical scroll, and its comment says exactly why:
|
||||
vertical scroll, and its comment says why:
|
||||
|
||||
> The cursor must follow the scroll: the renderer has an "auto-scroll
|
||||
> to keep cursor visible" pass that would otherwise snap `view_top`
|
||||
> straight back to wherever the cursor sits, so the user's mouse-wheel
|
||||
> scroll would feel stuck after one notch.
|
||||
|
||||
A horizontal analog hits the identical problem. Stage 3's Q#LL3
|
||||
deferred the choice here deliberately: **does explicit horizontal
|
||||
scroll drag the cursor, or does the next motion snap back?** That is
|
||||
**Q#HS4**.
|
||||
Under Q#HS2's answer (automatic-only) this pass is not a hazard but
|
||||
**the entire mechanism** — Stage 4 adds its horizontal component. The
|
||||
hazard returns with explicit commands, which is why Q#HS4 is deferred
|
||||
rather than closed.
|
||||
|
||||
### 1.6 `goal_col` exists and its relationship to `view_left` is unexamined
|
||||
|
||||
`goal_col` (`src/window.rs:376`) remembers a target column across
|
||||
vertical motion and is cleared at seven sites in `src/editor.rs`. It is
|
||||
a *column within the line*, not a viewport offset — but both are
|
||||
"horizontal position" state on the same window, and a design that
|
||||
ignores the interaction will produce a cursor that jumps on the first
|
||||
vertical motion after a horizontal scroll. Called out so it is designed
|
||||
rather than discovered.
|
||||
vertical motion, cleared at seven sites in `src/editor.rs`. It is a
|
||||
*column within the line*; `view_left` is a *viewport offset*. Both are
|
||||
horizontal state on the same window, and a design ignoring the
|
||||
interaction produces a cursor that jumps on the first vertical motion
|
||||
after a horizontal scroll. Feeds **Q#HS7**.
|
||||
|
||||
---
|
||||
|
||||
## 2. The scope question, stated before the answers
|
||||
## 2. The scope fact, stated before the answers
|
||||
|
||||
Stage 3 ended with `wrap` as the default. **Under `wrap`, horizontal
|
||||
scroll is meaningless** — there is nothing past the right edge. So
|
||||
Stage 4's entire surface is conditional on a buffer-local mode.
|
||||
|
||||
That is a coherence fact, not only an implementation one: one
|
||||
user-facing concept ("how do I see the rest of this line?") now has two
|
||||
disjoint answers depending on a setting, and the commands, key
|
||||
bindings, and status affordances for the `truncate` half do not exist
|
||||
under `wrap`. `COHERENCE.md` §20 requires this be stated. **Q#HS6.**
|
||||
**Under `wrap`, horizontal scroll is meaningless** — nothing sits past
|
||||
the right edge. So Stage 4's entire surface is conditional on a
|
||||
buffer-local mode: one user-facing question ("how do I see the rest of
|
||||
this line?") gets two disjoint answers depending on a setting. Stated
|
||||
here because `COHERENCE.md` §20 requires it, and answered in Q#HS6.
|
||||
|
||||
---
|
||||
|
||||
## 3. Open questions
|
||||
## 3. Questions
|
||||
|
||||
### Q#HS1 — is the GPU in scope for Stage 4?
|
||||
### Q#HS1 — is the GPU in scope? **ANSWERED: no — Stage 5**
|
||||
|
||||
The strongest argument for **yes**: Stage 3's entire thesis was that
|
||||
the two frontends should stop disagreeing by accident. Shipping
|
||||
horizontal scroll in the TUI only would recreate exactly the divergence
|
||||
`ui.line-wrap` was built to close — a `truncate` buffer would be
|
||||
navigable in one frontend and not the other.
|
||||
> **Answered 2026-08-07 (user):** split the GPU work into Stage 5.
|
||||
> *"This is a conscious, bounded divergence, not a repeat of Stage 3's
|
||||
> accidental one. Make the time box concrete and keep `wrap` default
|
||||
> until parity lands."*
|
||||
|
||||
The strongest argument for **no, name it Stage 5**: §1.3. The GPU needs
|
||||
a mechanism that does not exist yet, touching caret placement,
|
||||
decoration geometry, and hit testing. That is plausibly larger than the
|
||||
TUI half, and bundling them makes one reviewable change into two
|
||||
unreviewable ones.
|
||||
The distinction is the load-bearing part. Stage 3's defect was never
|
||||
"the frontends differ" — it was "the frontends differ and **nobody
|
||||
chose that**". A divergence that is decided, recorded, and bounded is a
|
||||
different object from one inherited from a library default.
|
||||
|
||||
**My vote: split it, and say so in the setting's description.** Ship
|
||||
the TUI half as Stage 4 and the GPU half as Stage 5, with the
|
||||
divergence *documented and time-boxed* rather than accidental — which
|
||||
is the distinction Stage 3 actually drew. Stage 3's defect was never
|
||||
"the frontends differ"; it was "the frontends differ and nobody chose
|
||||
that". But this is a product call about shipping a known asymmetry, and
|
||||
it is not mine to make.
|
||||
**The time box, concrete** (the user's requirement, and the part that
|
||||
makes this a decision rather than a deferral):
|
||||
|
||||
### Q#HS2 — what moves the viewport?
|
||||
1. **Stage 5 is the immediately-next QoL lane after Stage 4 merges** —
|
||||
not backlogged behind another arc. If something displaces it, that
|
||||
displacement is itself a decision to record here.
|
||||
2. **`wrap` stays the default until Stage 5 lands** (independently
|
||||
reaffirmed in Q#HS6). This is what keeps the divergence invisible to
|
||||
anyone who has not opted in: a default-configuration user is never
|
||||
exposed to it.
|
||||
3. **Stage 4's release notes must state the asymmetry** — horizontal
|
||||
scroll works in the TUI and not yet the GUI — in the same way #221's
|
||||
had to state the word-wrap loss.
|
||||
4. **While the gap exists, the `truncate` affordances must name it.**
|
||||
§6's description amendment is where that lands, so a GUI user
|
||||
choosing `truncate` learns the limitation from the setting rather
|
||||
than from the behavior.
|
||||
|
||||
Options, not mutually exclusive:
|
||||
### Q#HS2 — what moves the viewport? **ANSWERED: automatic only**
|
||||
|
||||
- **Automatic only** — the cursor-visibility pass gains a horizontal
|
||||
component, so moving the cursor past the edge scrolls the view. No
|
||||
new commands, no new bindings. Smallest surface; makes a long line
|
||||
readable by arrowing along it.
|
||||
- **Explicit commands** — `ui.scroll-left` / `ui.scroll-right`, bound
|
||||
or not, plus the `goal_col` and cursor-follow questions.
|
||||
- **Both**, which is what every editor with this feature ships.
|
||||
> **Answered 2026-08-07 (user):** *"Automatic-only first. It makes the
|
||||
> report's text reachable with no new command surface."*
|
||||
|
||||
**My vote: automatic first, as its own stage-within-a-stage.** It is
|
||||
the smallest change that makes the reported text *reachable*, and it
|
||||
needs no binding decisions. Explicit commands can follow with the
|
||||
evidence of use.
|
||||
The cursor-visibility pass gains a horizontal component, so moving the
|
||||
cursor past the edge scrolls the view. No new commands, no binding
|
||||
decisions, no new interaction island.
|
||||
|
||||
### Q#HS3 — per window or per buffer?
|
||||
Explicit `ui.scroll-left` / `ui.scroll-right` are **not** in Stage 4.
|
||||
They can follow with evidence of use, and they are what re-opens Q#HS4.
|
||||
|
||||
### Q#HS3 — per window or per buffer? **NOT ACTUALLY OPEN**
|
||||
|
||||
`view_left` is **per window**, unambiguously: two panes on one buffer
|
||||
must scroll independently, exactly as they already hold independent
|
||||
`view_top`s (`src/desktop.rs:92`). Stage 3's Q#LL2 already recorded
|
||||
this and accepted the consequence — the *mode* is buffer-local while
|
||||
the *offset* is per-window, so one user-facing concept spans two
|
||||
scopes.
|
||||
`view_top`s (`src/desktop.rs:92`). Stage 3's Q#LL2 recorded this and
|
||||
accepted the consequence — the *mode* is buffer-local while the
|
||||
*offset* is per-window, so one user-facing concept spans two scopes.
|
||||
|
||||
**This is not really open**; it is listed so the accepted split is
|
||||
re-confirmed at the moment it takes effect rather than inherited
|
||||
silently.
|
||||
Listed so the accepted split is re-confirmed where it takes effect
|
||||
rather than inherited silently.
|
||||
|
||||
### Q#HS4 — does the cursor follow an explicit scroll?
|
||||
### Q#HS4 — does the cursor follow an explicit scroll? **DEFERRED, not answered**
|
||||
|
||||
Only live if Q#HS2 includes explicit commands. §1.5 has the precedent
|
||||
and the hazard. **My vote: follow, matching `scroll_window`** — the
|
||||
existing snap-back pass makes the alternative feel broken, and the
|
||||
vertical behavior is already the answer users have been trained on
|
||||
*in this editor*.
|
||||
Not live under Q#HS2's answer: automatic-only means every viewport move
|
||||
already originates from a cursor move. §1.5 holds the precedent and the
|
||||
hazard for whenever explicit commands arrive.
|
||||
|
||||
### Q#HS5 — does `view_left` survive a restart?
|
||||
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**
|
||||
|
||||
`view_top` does (§1.4). Consistency argues yes; a defaulted field
|
||||
avoids a `DESKTOP_VERSION` bump.
|
||||
|
||||
**My vote: yes, defaulted, no version bump** — but confirm that
|
||||
**My vote: yes, defaulted, no version bump** — but **confirm
|
||||
`SavedLeaf`'s deserializer tolerates a missing field before relying on
|
||||
it, because §1.4 is a citation of the *shape*, not of serde's
|
||||
behavior on it.
|
||||
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.
|
||||
|
||||
### Q#HS6 — what does the coherence statement say?
|
||||
### Q#HS6 — the coherence statement **ANSWERED: keep `wrap` default**
|
||||
|
||||
Per `COHERENCE.md` §20 this framing must state its coherence impact.
|
||||
The honest version is uncomfortable: Stage 4 adds capability that
|
||||
exists **only under a non-default mode**, which is a new conditional
|
||||
surface rather than a uniform improvement. Journey step 4 ("Understand
|
||||
interface") is the row it serves.
|
||||
> **Answered 2026-08-07 (user):** *"Keep `wrap` as default for now.
|
||||
> Revisit only after GPU parity and use evidence; changing to
|
||||
> `truncate` before Stage 5 would make the default unreachable in the
|
||||
> GUI."*
|
||||
|
||||
**Q#HS6 is whether that is acceptable, or whether the arc should
|
||||
instead reconsider the default.** Naming the alternative honestly: if
|
||||
horizontal scroll makes `truncate` genuinely good, `wrap` being the
|
||||
default is a choice worth re-examining rather than treating as settled
|
||||
— and Stage 3 chose it partly *because* scroll did not exist.
|
||||
That last clause is the argument revision 1 missed. I had framed this
|
||||
as "if scroll makes `truncate` good, the default deserves
|
||||
re-examination" — but with the GPU deferred to Stage 5, a `truncate`
|
||||
default would ship a mode that is **navigable in the TUI and a dead end
|
||||
in the GUI**, for every user who never opened the setting. Q#HS1 and
|
||||
Q#HS6 are therefore coupled: the split is only safe *because* the
|
||||
default does not move.
|
||||
|
||||
Revisit after Stage 5, on use evidence, not before.
|
||||
|
||||
### Q#HS7 — what IS `view_left`? **NEW, BLOCKING**
|
||||
|
||||
**Revision 1 decided what moves the viewport without ever saying what
|
||||
the viewport offset is.** That is the same omission Stage 3 would have
|
||||
made had it shipped `WrapMode` without `DisplayCoord`: the mode is
|
||||
useless until the coordinate contract is written down, and the contract
|
||||
is where every sharp edge lives.
|
||||
|
||||
Revision 1's verification sketch named tabs and wide characters. **It
|
||||
had no oracle for either**, because nothing defined what a left edge
|
||||
is. Four things must be settled together:
|
||||
|
||||
**(a) The unit.** Candidates: a source byte offset within the line; a
|
||||
display column (cells from the line start, after tab expansion); or a
|
||||
cell boundary with an explicit validity rule.
|
||||
|
||||
*My vote: display column.* Tab expansion depends on the absolute column
|
||||
from the line start, so the walk must begin at column 0 and compute
|
||||
forward **regardless** of the offset — which makes a byte offset buy
|
||||
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.
|
||||
|
||||
*My vote: a left edge may not fall inside a wide glyph's cells.*
|
||||
|
||||
**(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.
|
||||
|
||||
*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.
|
||||
|
||||
**(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.
|
||||
|
||||
*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.
|
||||
|
||||
---
|
||||
|
||||
## 4. Verification sketch (not final — depends on Q#HS1/HS2)
|
||||
## 4. Verification sketch (depends on Q#HS7)
|
||||
|
||||
- Cell-level tests at several window widths with a non-zero offset —
|
||||
which is the Stage 4 case Stage 3's sketch explicitly refused to
|
||||
write, because "at non-zero offset" was a `view_left` requirement
|
||||
smuggled into a wrap lane.
|
||||
- Cell-level tests at several window widths **at non-zero offset** —
|
||||
the case Stage 3's sketch explicitly refused, because "at non-zero
|
||||
offset" was a `view_left` requirement smuggled into a wrap lane.
|
||||
- **A `wrap` control for every claim**, asserting the wrap path is
|
||||
byte-identical with a horizontal offset present, since under `wrap`
|
||||
the offset must be inert.
|
||||
byte-identical with a horizontal offset present. Under `wrap` the
|
||||
offset must be **inert**, not merely harmless.
|
||||
- Round-trip identity for `place_of_byte` / `byte_at_place` at non-zero
|
||||
offset, including the wide-character and tab cases Stage 3 settled at
|
||||
offset 0.
|
||||
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; a snap request landing inside each.
|
||||
- **A PTY acceptance test for reachability**, following
|
||||
`tests/long_line_readable_acceptance.rs`: in `truncate`, the tail of
|
||||
a long line must reach the terminal *after* whatever Q#HS2 chooses
|
||||
moves the view. That file's `truncate` control currently asserts the
|
||||
tail is **absent** — Stage 4 must update it, and that update is
|
||||
itself the proof the caveat is gone.
|
||||
- If the GPU is in scope: a headless witness that the caret, a
|
||||
decoration, and a hit test all agree with the shifted origin — the
|
||||
three consumers §1.3 names.
|
||||
`tests/long_line_readable_acceptance.rs`. That file's `truncate`
|
||||
control currently asserts the tail is **absent** — Stage 4 must
|
||||
update it, and **that update is itself the proof the caveat is
|
||||
gone**.
|
||||
- **No GPU witness in Stage 4** (Q#HS1). Stage 5 owes one that the
|
||||
caret, a decoration, and a hit test all agree with the shifted
|
||||
origin — the three consumers §1.3 names.
|
||||
|
||||
---
|
||||
|
||||
## 5. Coherence impact (§20 requirement)
|
||||
|
||||
- **Journey step 4, "Understand interface — Partial."** Stage 3's
|
||||
framing said the scorecard should name "a line that cannot be read in
|
||||
full" against this step. Stage 4 completes that only for `truncate`.
|
||||
- **§16 Semantic Frontend Architecture.** Q#HS1 decides whether this
|
||||
lane *narrows* or *widens* frontend divergence. If the GPU is
|
||||
deferred, the divergence is deliberate and time-boxed — which is
|
||||
materially different from Stage 3's inherited accident, and the
|
||||
release notes must say which kind it is.
|
||||
- **No new interaction island** if Q#HS2 lands automatic-only.
|
||||
Explicit commands would go in the ordinary command registry, not a
|
||||
new surface.
|
||||
- **Config registry adoption**: none new expected. Stage 4 navigates
|
||||
the mode Stage 3 declared; if it needs a setting, that is a signal
|
||||
the design has drifted.
|
||||
- **Journey step 4, "Understand interface — Partial."** Stage 4
|
||||
completes "a line that cannot be read in full" for `truncate` **in
|
||||
the TUI only**. The scorecard row should say so rather than reading
|
||||
as closed.
|
||||
- **§16 Semantic Frontend Architecture.** Q#HS1 *widens* frontend
|
||||
divergence for the duration of the Stage 4→5 gap. Per the answer,
|
||||
this is deliberate and time-boxed, and materially different from
|
||||
Stage 3's inherited accident — but the release notes must say which
|
||||
kind it is, or a reader cannot tell them apart.
|
||||
- **No new interaction island** — automatic-only adds no command
|
||||
surface (Q#HS2).
|
||||
- **Config registry: no new settings expected.** Stage 4 navigates the
|
||||
mode Stage 3 declared. If it needs a setting, that is a signal the
|
||||
design has drifted, not a feature.
|
||||
|
||||
---
|
||||
|
||||
## 6. A Stage 4 deliverable that is not scroll
|
||||
|
||||
**Amend `ui.line-wrap`'s description** (`builtin/runtime/linewrap.lua`).
|
||||
Today it says only *"…or truncate at the edge"*, which does not tell a
|
||||
user that the edge is a wall. The honest text depends on where Stage 4
|
||||
lands:
|
||||
|
||||
- **Before Stage 4**, `truncate` means unreachable in both frontends.
|
||||
- **After Stage 4**, it means reachable in the TUI and unreachable in
|
||||
the GUI until Stage 5 (Q#HS1's time box, item 4).
|
||||
- **After Stage 5**, the caveat is gone and the sentence should shrink
|
||||
back.
|
||||
|
||||
Carried here rather than filed elsewhere because it is the one place
|
||||
the arc's user-visible honesty is currently wrong, and revision 1
|
||||
asserted it was already right.
|
||||
|
|
|
|||
Loading…
Reference in New Issue