1839 lines
91 KiB
Markdown
1839 lines
91 KiB
Markdown
# Long lines — QoL Stage 3
|
|
|
|
**Status: revision 20 — APPROVED (2026-08-06). All eight questions
|
|
answered; §5d.6 resolved. Implementation complete; PR pending.**
|
|
|
|
**Revision 2** corrected a load-bearing error in revision 1: it claimed
|
|
both frontends render from the same `CellGrid`. They do not — the GPU
|
|
ignores the grid variants and lays out locally, and it **already wraps
|
|
long lines**. See §1.2. The correction reverses the cost profile and
|
|
therefore the recommendation in §3.
|
|
|
|
**Revision 3** fixed two things review caught downstream of that
|
|
correction. The opening still called the defect "total ... in either
|
|
frontend," contradicting §1.2's own table — corrected below, along with
|
|
what the cross-frontend defect actually is. And §7's GPU witness
|
|
described the non-wrap mode as "scroll/truncate," conflating a deferred
|
|
Stage 4 capability with a Stage 3 mode; **§3.1 now states the Stage 3
|
|
surface explicitly** rather than letting a test mode imply a product
|
|
one.
|
|
|
|
Writing §3.1 surfaced a third thing neither review nor revision 2 had
|
|
named: agreeing on the mode is not agreeing on the wrap. **New Q#LL5
|
|
(§5a)** — the GPU wraps at `Wrap::WordOrGlyph` and a grid walk would
|
|
naturally wrap at the character, so both frontends could honor `wrap`
|
|
and still break lines in different places. It was briefly buried in
|
|
"not in scope"; it is the same class of defect this lane exists to
|
|
close, so it is a question now.
|
|
|
|
**Revision 4** — the verification sketch still asked
|
|
for `pos_to_display` / `display_to_pos` round trips "at non-zero
|
|
offset", and its first bullet for cell tests "at several offsets".
|
|
Both are `view_left` requirements, and `view_left` is Stage 4.
|
|
Deferring scope in §3.1 and §9 while test lines quietly assumed it is
|
|
exactly how deferred work creeps back in. Replaced with the wrapped
|
|
visual-row mapping witness `wrap` actually needs — including the
|
|
wrap-point boundary case and a `truncate` control — and with "at
|
|
several window widths" (§7).
|
|
|
|
**Revision 5** — revision 4's replacement then asked
|
|
for round-trip identity "for every position", which the existing
|
|
coordinate contract makes impossible: `pos_to_display` canonicalizes a
|
|
byte inside a multi-byte codepoint to that codepoint's column, and
|
|
`display_to_pos` returns the codepoint start. Restated as identity on
|
|
valid cursor boundaries plus projection elsewhere, with the
|
|
interior-byte canonicalization preserved under its own separate witness
|
|
(§7). A witness that cannot be satisfied gets weakened until it passes,
|
|
which would have cost exactly the discriminating power §7 exists for.
|
|
|
|
**Revision 6** — the wrap-point case still said the
|
|
last position on row *k* and the first on row *k+1* "must not
|
|
collide". They are **one** source position with two candidate
|
|
coordinates, so that assertion has no content. §7 now *decides* the
|
|
ownership — the wrap position is column 0 of the next row, because the
|
|
alternative coordinate is off-grid on a row that is full by
|
|
construction — states affinity as a deliberate non-goal, and separately
|
|
names the requirement revision 5 was actually reaching for: the two
|
|
*distinct* adjacent codepoint starts across the break must map
|
|
distinctly.
|
|
|
|
**Revision 7** — revision 6's justification was wrong
|
|
twice. A hard line ending at exactly `max_cols` also has column
|
|
`max_cols` — `pos_to_display` clamps nothing — so "off-grid" never
|
|
distinguished the soft-wrap case; and `pos_to_display` takes no
|
|
viewport at all, so it has no grid to be off. The decision stands, on a
|
|
rule that subsumes both cases: **a position maps to the cell of the
|
|
glyph that follows it when one exists, otherwise just past the last
|
|
glyph** — which resolves the soft wrap and leaves hard ends, including
|
|
full-row ones, exactly as they are today (now a control in §7).
|
|
|
|
Chasing that also surfaced a structural cost no earlier revision had:
|
|
under `wrap`, `pos_to_display` **cannot compute a visual row from its
|
|
current arguments**, so the wrap width has to reach it — a trait
|
|
signature change across ~35 call sites, though only `TextView`
|
|
overrides the method.
|
|
|
|
**Revision 8** — that cost was still framed too
|
|
narrowly: a signature is not a model. `display_to_pos` has the same
|
|
missing inputs and treats `coord.row` as a raw source-line index, so it
|
|
fails *silently* into the wrong line; and `view_top`, vertical motion,
|
|
paging, wheel scroll, gutters and overlays all operate in source-line
|
|
space today. `move_down` alone treats a display row as a source line.
|
|
**New Q#LL6 (§5b)** makes the
|
|
authoritative logical-to-visual row map a design item — both
|
|
directions, its width/mode inputs, how it *composes with* the existing
|
|
fold map rather than bypassing it, what becomes of `view_top` (a
|
|
persisted value, via `saveplace`), and `truncate` as the identity case.
|
|
§5b.7 restates the cost as an audit of both mapping APIs and every
|
|
source-row assumption.
|
|
|
|
**Revision 9** — revision 8's `view_top` question was
|
|
a **false binary**: "source line" and "visual row" are both
|
|
unworkable. A source line cannot name a viewport starting partway down
|
|
a wrapped line, so a line taller than the viewport could never scroll
|
|
to its second visual row — the exact buffers this lane is for. The
|
|
representation must be composite (anchor line + row-within-line),
|
|
composed with folds.
|
|
|
|
The persistence consequence is also sharper than "a format change".
|
|
`saveplace` has **no version marker** and stores a bare integer, so
|
|
redefining `view_top` silently reinterprets every existing record; and
|
|
its path field is the whitespace-split remainder, so appending a field
|
|
is not backward-compatible either. A visual row is additionally
|
|
**width-dependent** — saved at 120 columns, restored at 80, it denotes
|
|
a different place. §5b.6 is new, recommends persisting only the
|
|
width-independent anchor line (no migration needed, by construction),
|
|
and requires Q#LL6 to settle a **resize-restore policy** for the
|
|
row-within-line offset, which live resizes need regardless of what is
|
|
stored.
|
|
|
|
Also: revisions 4 through 8 each ended up labelled "the current one".
|
|
Only the Status line above is authoritative; the stale markers are
|
|
removed.
|
|
|
|
**Revision 10** — revision 9 described
|
|
`VisibleLineMap` as mapping source lines to a renumbered *visible-line*
|
|
space. It does not: `next_visible`, `prev_visible`, `visible_head_of`
|
|
and `clamp_view_top` all take and return **source-line indices**,
|
|
constrained to visible heads (`src/fold_view.rs:223`). Folds project
|
|
onto visible anchors; they do not renumber. The error mattered in the one place
|
|
this section exists to protect — a reader who believed folds renumber
|
|
would add a *second* renumbering for wrap and misindex every fold
|
|
consumer. §5b.2, §5b.3 and §5b.4 are corrected, and the same loose
|
|
wording is fixed in §2.1, §5 and §7, where it had also mislabelled
|
|
`pos_to_display`'s current return (a **source** line index, from
|
|
`line_at_offset`).
|
|
|
|
§5b.6 additionally turns the anchor-persistence recommendation into an
|
|
explicit **public API contract**: `pmacs.editor.view_top()` keeps
|
|
returning the source anchor and `set_view_top(n)` sets it with
|
|
`row_within_line = 0`, so `saveplace` needs no change and existing
|
|
records keep working by contract rather than by luck.
|
|
|
|
**Revision 11** — revision 10 replaced "renumbers"
|
|
with "restricts the source-line domain to visible heads". The second
|
|
half is also wrong, and in a way that inverts a contract:
|
|
`clamp_view_top` **deliberately accepts a hidden line** and projects it
|
|
to its visible head — that is why it exists (`src/fold_view.rs:215`),
|
|
and `text_view::render` depends on it. "Restricted domain" would make
|
|
the supported case read as a caller error. §5b.3 now describes the
|
|
first step as a **source-index-preserving projection onto a visible
|
|
source-line anchor**: total, idempotent, same index space — which is
|
|
the same shape as §7's coordinate rule one level up. The load-bearing
|
|
conclusion is unchanged: no dense middle coordinate space exists.
|
|
|
|
**Revision 12** — revision 11 stated that shared rule
|
|
as "projection to the **nearest** canonical value". It is not
|
|
proximity-based: a hidden line maps to its fold head even when the next
|
|
visible line is closer, and an interior UTF-8 byte maps to its
|
|
codepoint start rather than the nearer boundary. Nor is it uniformly
|
|
backward — `pos_to_display` projects an interior byte back to the
|
|
codepoint start while `display_to_pos` rounds forward to the next
|
|
boundary. The accurate rule is **identity on canonical inputs;
|
|
otherwise projection to the contract's designated canonical
|
|
representative**, with total-and-idempotent as the genuinely shared
|
|
algebra (§5b.3).
|
|
|
|
**Revision 13** — records decisions from the
|
|
2026-08-06 design discussion rather than correcting an error.
|
|
|
|
**Q#LL1 is answered** (§3): `wrap` + `truncate`, **default `wrap`**,
|
|
scroll deferred to Stage 4. The default is a knowing behavior change to
|
|
the TUI — no default can preserve both frontends, because they
|
|
currently disagree.
|
|
|
|
**Q#LL6 item 3 is answered** (§5b.4): `view_top`'s sub-line component
|
|
is a **byte**, not a row index — width-independent, exactly reversible
|
|
across resizes, and it **dissolves** the resize-restore policy §5b.6
|
|
demanded rather than answering it. Two further structural decisions are
|
|
recorded in the new §5b.5: **no global map is needed** (every vertical
|
|
consumer is local, so layout is per-line — which removes the
|
|
`open_100mb_under_200ms` risk §5b.7's framing invited), and
|
|
**`DisplayCoord` gains a `sub_row` rather than redefining `row`**, so
|
|
untouched consumers stay correct instead of merely findable.
|
|
|
|
Found while answering: **the GPU already carries this composite
|
|
anchor** — `scroll_top` plus `code_scroll_residual`, renormalized by
|
|
`normalize_code_scroll` (framing Q#F6) when reflow pushes the residual
|
|
across source lines. The shape is precedent, not invention.
|
|
|
|
**Revision 14** — answers the remaining four questions
|
|
and moves the document to APPROVED.
|
|
|
|
**Q#LL2** (§4): buffer-local, with `Viewport` carrying the *resolved*
|
|
mode as it already carries `folds`, so `TextView` stays
|
|
config-agnostic. **Q#LL4** (§6): do not adopt `editing.fill-column`;
|
|
name ours `ui.line-wrap`. *(Revision 20 corrects the stated reason:
|
|
the setting is a `#[cfg(test)]` fixture name, not an orphaned
|
|
definition — see §1.1, withdrawn. The answer is unchanged, the
|
|
"sharpen its description" deliverable is void, and naming the fixtures
|
|
as fixtures replaces it.)* **Q#LL5** (§5a): character wrap in **both** frontends,
|
|
accepting that GUI users lose word wrap — the analysis changed on
|
|
discovering that a whitespace-based grid wrap would give only
|
|
*approximate* parity against cosmic-text's UAX #14 line breaking, which
|
|
is worse than honest divergence. **Q#LL6 items 1-2** (§5b.4):
|
|
`TextView` methods, no cache initially, one `Copy` context parameter —
|
|
breaking on the input side so the compiler enumerates the audit,
|
|
additive on the output side so untouched consumers stay correct.
|
|
|
|
**Revision 15** — review of `bd752f2` found two holes
|
|
and one notation hazard.
|
|
|
|
**Q#LL7 (§5c) — the GPU had no wire.** §4 resolves the mode into
|
|
`Viewport`, which reaches the *grid*. The GPU is not a grid consumer:
|
|
it lays out locally, `BufferSnapshot` carries only CRDT bytes, and no
|
|
message expresses a wrap mode. So `truncate` would have changed the TUI
|
|
and left the GPU wrapping — the exact disagreement this lane closes.
|
|
Specified as an additive variant at v22 (advertised baseline unmoved),
|
|
carrying `buffer_id` because the mode is buffer-local, resent on
|
|
attach, on config change, **and on buffer switch** — the third being
|
|
the one a `FontFacts`-shaped design misses, since font size is global
|
|
while wrap mode is per buffer.
|
|
|
|
**Q#LL8 (§5d) — "every vertical consumer is local" was false.** The
|
|
scroll indicator needs a total: a one-line buffer wrapping to fifty
|
|
rows has `total_lines == 1`, so `format_scroll_indicator` returns
|
|
`All` while forty-nine rows sit off-screen. §5b.5 is narrowed
|
|
accordingly, keeping the distinction that bounds the cost — a *total*
|
|
is one lazily-computed number, an *index* is `O(N)` resident. `All`,
|
|
`Top` and `Bot` need no aggregate at all; only `NN%` does.
|
|
|
|
**Notation (§7).** Revision 14 said `pos_to_display` returns "the
|
|
visual row", which reads as redefining `row` — the thing §5b.5
|
|
forbids. Every wrap-point example is now the explicit triple
|
|
`{row, sub_row, col}`, and both coordinates at a soft break share the
|
|
same `row`, which is the information a redefinition would have
|
|
destroyed.
|
|
|
|
**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.
|
|
|
|
**The GPU has its own indicator** (§5d.3). `format_scroll_indicator` is
|
|
**duplicated, not shared** — `src/editor.rs:5509` and
|
|
`pmacs-gpu/src/main.rs:10114` — and the GPU passes
|
|
`current_line_starts.len()`, a source-line count. So revision 15 would
|
|
have fixed the indicator in the TUI and left the GPU reporting `All`
|
|
for a one-line wrapped buffer: **this lane's own defect, reproduced by
|
|
the section meant to close it.** Both copies keep their signature; what
|
|
changes is what the callers pass, so every existing formatter test
|
|
stays valid.
|
|
|
|
**The lazy total's cache key omitted fold state** (§5d.2). Folds are
|
|
per rendered window and can change with no edit, no resize and no mode
|
|
change, so all three of revision 15's key components stay put while the
|
|
projection moves. Corrected to **(buffer generation, content width,
|
|
mode, fold projection)** — keyed on the projection's own `components`,
|
|
which is `O(folds)` to compare and **cannot be forgotten**, rather than
|
|
a maintained revision counter that can. Same principle as byte-anchoring
|
|
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** — 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.7 adds the **large-file guard witnesses**, because "no
|
|
whole-document work happens" must be enforced, not merely intended.
|
|
|
|
**Revision 18** — review of `d6b5285` found the
|
|
interface contradiction the byte fallback left behind. Revision 16 had
|
|
claimed the formatter could keep its signature and change only its
|
|
arguments; it cannot. **Every branch of `format_scroll_indicator`
|
|
derives from `total_lines`** — including `Bot` via
|
|
`view_top + visible >= total_lines`, which with a byte total would
|
|
compare **rows against bytes** — and any stand-in small enough to pass
|
|
restores the false `All`. "Local predicates" is not something that
|
|
signature can express, because it has no parameter for them.
|
|
|
|
**Resolved by not asking it to (§5d.5).** `truncate` calls the existing
|
|
formatter **untouched**, so its output is byte-identical *by
|
|
construction* and every existing formatter test stays valid; `wrap`
|
|
calls a new `classify(first_visible, last_visible, byte_pos, byte_len)`
|
|
returning `All`/`Top`/`Bot`/`Percent`, which never sees a row count —
|
|
so the unit mixing is not avoided but **unrepresentable**. Trying to
|
|
serve two genuinely different contracts from one four-count signature
|
|
was the mistake; it could only do so by making units implicit, which is
|
|
how the contradiction arose.
|
|
|
|
**§5d.6 is a new open question**: `pmacs-gpu` depends on
|
|
`pmacs-protocol` only, never on the `pmacs` lib, so the duplication is
|
|
**structural**. Duplicating the classifier preserves exactly the
|
|
condition that produced §5d.3's defect; sharing it via
|
|
`pmacs-protocol` makes agreement structural but widens that crate
|
|
toward presentation — a §16 layering call I am not taking alone.
|
|
|
|
**Revision 20 — the current one.** Written *during* implementation
|
|
rather than before it, because that is when the error surfaced: §1.1's
|
|
"orphaned setting" does not exist. `editing.fill-column` is a
|
|
`#[cfg(test)]` fixture name at both cited sites, so the Q#LL4
|
|
deliverable "sharpen its description" had no object. §1.1 is withdrawn
|
|
in place with the original text and the reasoning that produced it;
|
|
§6's answer is unchanged and its premise corrected.
|
|
|
|
The approval is not reopened by this. Q#LL4's answer was *do not adopt
|
|
it*, and a setting that turns out not to exist is a stronger reason not
|
|
to adopt it than the one recorded. Nothing else in the document
|
|
depended on §1.1 — it argued for a display setting separate from
|
|
`fill-column`, which is what shipped.
|
|
|
|
**Revision 19.** §5d.6 answered as **(b)**:
|
|
`ScrollPosition` and `classify` go in `pmacs-protocol`, string
|
|
rendering stays per-frontend. The §16 reading that decides it — each
|
|
frontend computes its own local layout facts, the shared crate owns the
|
|
semantic decision they feed — is a sharper split than "shared
|
|
vocabulary", and it adds **no wire message and no version bump**.
|
|
Q#LL8 is approved. **The framing is approved; implementation begins.**
|
|
|
|
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.
|
|
|
|
The user's report, from daily-driver use:
|
|
|
|
> long lines need to either wrap somehow or be scrollable. Haven't
|
|
> tried this in GUI, but in TUI, a line that extends off screen cannot
|
|
> be read in full in any way. This should also be something that the
|
|
> user can configure, whether to wrap or scrollable.
|
|
|
|
**The report is accurate where it makes a claim, and it is careful to
|
|
limit that claim to the TUI.** In the TUI a line wider than the window
|
|
is unreadable past the edge by any means. The GPU is not in that state:
|
|
it already wraps, so the text is readable there --- see §1.2, which
|
|
also records that revision 1 of this document asserted "in either
|
|
frontend" and was wrong to.
|
|
|
|
**The cross-frontend defect is not unreadability. It is that neither
|
|
behavior was chosen.** The TUI truncates because a cell walk breaks at
|
|
`max_cols`; the GPU wraps because a library default was never
|
|
overridden. Two accidents that disagree, and no way for the user to
|
|
express a preference in either --- which is the part of the report that
|
|
applies to both frontends: *"should also be something that the user can
|
|
configure."*
|
|
|
|
---
|
|
|
|
## 1. What is already built
|
|
|
|
Almost nothing, and that is the honest headline. Stage 1 and Stage 2
|
|
both drove machinery that already existed --- Stage 1 honored a flag
|
|
with a documented contract, Stage 2 drove a font preference with a
|
|
whole wire message behind it. **Stage 3 has no such seam.** It builds a
|
|
capability the codebase does not have.
|
|
|
|
What exists and helps:
|
|
|
|
- **`text_view::render` is the single place a source line becomes
|
|
cells** (`src/text_view.rs:211`). One walk, one truncation site ---
|
|
**for the grid path only.** See §1.2: that is one of two renderers,
|
|
not the renderer.
|
|
- **The fold precedent.** Arc 6 already broke the row-to-source-line
|
|
identity: `VisibleLineMap` + `Viewport.folds` let row `r` show the
|
|
`r`-th *visible* line. The framing for it recorded why folding is not
|
|
an overlay --- *"overlays repaint cells, they cannot delete rows"*
|
|
(`src/view.rs`). Wrapping is the exact dual: it **adds** rows for one
|
|
source line. The same argument forbids it being an overlay, for the
|
|
same reason.
|
|
- **The config registry already supports buffer-local overrides.**
|
|
`Registry::get(name, Option<BufferId>)` consults a per-buffer layer
|
|
before global (`src/config_registry.rs:843`), with an explicit note
|
|
that there is no ambient current buffer --- a caller wanting
|
|
buffer-aware behavior must pass the `BufferId`. So "wrap in prose,
|
|
scroll in logs" needs no new machinery.
|
|
|
|
What does **not** exist:
|
|
|
|
- **No horizontal offset anywhere.** `Viewport` (`src/view.rs:131`) has
|
|
`buffer_start`, `buffer_end`, `cell_origin`, `cell_size`, `gutter_w`,
|
|
`folds` --- and no column offset. `Window` has `view_top`
|
|
(`src/desktop.rs:92`) and no `view_left`.
|
|
- **The truncation is one line of code with no alternative path.**
|
|
`src/text_view.rs:251`:
|
|
```rust
|
|
if col >= max_cols {
|
|
break;
|
|
}
|
|
```
|
|
The walk always starts at the line's first character. There is no
|
|
mode, no flag, and no caller that can ask for anything else.
|
|
|
|
### 1.1 ~~An orphaned setting, of the exact shape Stage 1 just fixed~~ **WITHDRAWN (revision 20)**
|
|
|
|
> **This section was wrong, and the error survived nineteen revisions
|
|
> and three rounds of review.** It is kept rather than deleted because
|
|
> the failure mode is reusable: a grep hit that *reads* like production
|
|
> code.
|
|
>
|
|
> `editing.fill-column` is **not defined in the registry**. Both
|
|
> occurrences are inside `#[cfg(test)] mod tests` ---
|
|
> `src/config_registry.rs:1191` and `src/lua_bindings/config.rs:898`
|
|
> --- where they are fixture names in round-trip tests that exercise
|
|
> one setting per `ConfigKind`. Two of the five names in those fixtures
|
|
> are real (`editing.auto-pair`, `autosave.interval-ms`); the other
|
|
> three, including this one, are defined nowhere else in the tree.
|
|
>
|
|
> So there is no shipped setting. No user can get it, set it, or
|
|
> discover it, and **there is no description to sharpen** --- which was
|
|
> the one Q#LL4 deliverable this document still owed. What Stage 3 does
|
|
> instead is name the fixtures as fixtures at both sites, so the next
|
|
> reader is not sent down the same path.
|
|
>
|
|
> The original claim, for the record: *"`editing.fill-column` is
|
|
> defined in the registry --- 'Preferred wrap column.',
|
|
> `ConfigKind::Number`, min 1, max 1000 --- and is read by nothing.
|
|
> This is the same shape as the `full_grid` defect Stage 1 closed."*
|
|
>
|
|
> **How it happened.** I grepped for prior art on line width, found the
|
|
> name at a `src/` path, read the `r.define(...)` call --- which is
|
|
> genuine API usage, not a mock --- and never checked the enclosing
|
|
> module. `#[cfg(test)]` sat about fifty lines above. Every later
|
|
> revision inherited the conclusion instead of the evidence, and each
|
|
> round of review reasoned about the *consequences* of an orphaned
|
|
> setting rather than re-checking that it existed. A file:line citation
|
|
> is not a substitute for reading the scope it sits in.
|
|
|
|
The surviving observation, which needed none of the above: `fill-column`
|
|
is a *fill* concept (where `M-q` reflows text, editing the buffer), not
|
|
a *display wrap* concept (where a long line is shown across rows, buffer
|
|
unchanged). Conflating them is a known Emacs papercut, and it is why
|
|
this lane's setting is `ui.line-wrap` rather than a reuse of that name
|
|
--- see Q#LL4 (§6).
|
|
|
|
### 1.2 The two frontends already disagree, and that is the defect
|
|
|
|
**Revision 1 of this document got this wrong and the error inverted the
|
|
lane's cost.** It claimed both frontends consume the same `CellGrid`,
|
|
so one change in `text_view::render` would reach both for free. That is
|
|
false, and `pmacs-gpu` says so in its own words
|
|
(`pmacs-gpu/src/main.rs:4502`):
|
|
|
|
> The grid variants (`CellDelta`, `Cursor`, `CursorByte`) are
|
|
> **ignored** --- pmacs-gpu **lays out locally** and tracks the cursor
|
|
> via `PresenceUpdate`.
|
|
|
|
The `terminal.rs:772` comment revision 1 cited --- "one run per row,
|
|
never wrapped together" --- is the **vterm** path, where a run must
|
|
occupy exactly the cells the child gave it. It says nothing about
|
|
document text.
|
|
|
|
What the GPU actually does with a long line: **it wraps it.** Every
|
|
explicit `set_wrap` in `pmacs-gpu` is `Wrap::None` and every one is
|
|
chrome --- status, status-left, menu, minibuffer, completion --- or the
|
|
terminal run path (`:3930`, `:3940`, `:3956`, `:3966`, `:3978`,
|
|
`:5313`, `:5618`, `:8215-8221`). **The document buffer never sets a
|
|
wrap mode at all**, so it keeps the one `cosmic-text`'s `Buffer`
|
|
constructor installs --- `Wrap::WordOrGlyph` (`buffer.rs:262` in
|
|
0.18.2) --- which is what `sync_buffer_dimensions`'s comment assumes:
|
|
*"so cosmic-text wraps at the final clip"* (`:4338`).
|
|
|
|
It is tested, if incidentally: `wrapped_caret_survives_size_changes`
|
|
(`:15853`) puts a 180-character line in a 320px window with the caret
|
|
at byte 180 and asserts the caret paints inside the code clip. A
|
|
truncating renderer would have that byte off-screen.
|
|
|
|
**So the real state of the product is:**
|
|
|
|
| | long line | horizontal scroll |
|
|
|---|---|---|
|
|
| TUI (grid) | **truncated, unreadable** | none |
|
|
| GPU (local layout) | **wrapped** --- readable | none |
|
|
|
|
This reframes the lane. The user wrote *"Haven't tried this in GUI, but
|
|
in TUI, a line that extends off screen cannot be read in full"* --- and
|
|
that instinct was exactly right. The GUI half most likely already
|
|
works. What is broken there is different: **the wrap is implicit,
|
|
unconfigurable, and was never a decision** --- it is cosmic-text's
|
|
default leaking through as product behavior.
|
|
|
|
**Stage 3 is therefore not "build wrap and scroll." It is "make the two
|
|
frontends agree on a mode the user chose."** That is a §16 Semantic
|
|
Frontend Architecture concern, and it is a larger lane than revision 1
|
|
implied: the work lands in `text_view::render` *and* in the GPU's local
|
|
layout, with a shared setting deciding both.
|
|
|
|
|
|
---
|
|
|
|
## 2. The central question: wrap and scroll are not one mechanism
|
|
|
|
The user's phrasing --- "either wrap somehow or be scrollable ...
|
|
whether to wrap or scrollable" --- reads as one setting with two
|
|
values. **Internally they are not two settings on one mechanism; they
|
|
are two different mechanisms**, and the framing has to say so before
|
|
anything is built.
|
|
|
|
- **Horizontal scroll** is a *viewport offset*. Row `r` still shows
|
|
exactly one source line. The walk starts at display column
|
|
`view_left` instead of 0. The row-to-line relation is untouched.
|
|
- **Wrap** is a *row-multiplying line map*. One source line occupies
|
|
ceil(width / cols) rows. The row-to-line relation **breaks**, in the
|
|
same way folding broke it --- and in the opposite direction.
|
|
|
|
They cost very different amounts. Scroll is close to the cheap change
|
|
it looks like. Wrap is not.
|
|
|
|
### 2.1 What wrap breaks that scroll does not
|
|
|
|
`TextView::pos_to_display` (`src/text_view.rs:156`) returns
|
|
`DisplayCoord::new(row_idx, col)` where `row_idx` is the **source**
|
|
line index (`line_at_offset(pos)`) and `col` is the display width of
|
|
the line's prefix. Under wrap
|
|
neither half survives: one source line has many rows, and `col` is
|
|
width-modulo-cols rather than the prefix width.
|
|
|
|
That function is not a detail. Per `src/view.rs`, **cursor placement
|
|
and scrolling use the base text view's `pos_to_display` only.** And
|
|
`overlay_paint.rs` maps every overlay's display row through `view_top`
|
|
plus the fold map (`src/overlay_paint.rs:178`, `:318`). So a new row
|
|
mapping that does not go through the same place puts **every overlay
|
|
--- diagnostics, highlights, inlay hints --- on the wrong row** for any
|
|
buffer containing a wrapped line.
|
|
|
|
This is the real cost of wrap, and it is why I am not proposing to
|
|
build both at once.
|
|
|
|
---
|
|
|
|
## 3. Q#LL1 --- scope: one mechanism or two? **ANSWERED**
|
|
|
|
> **Answered 2026-08-06.** Stage 3 ships `wrap` and `truncate`, with
|
|
> **`wrap` as the default**; horizontal scroll is Stage 4. The default
|
|
> was decided knowingly: no default preserves both frontends, because
|
|
> they currently disagree (§1.2), so this one **changes the TUI's
|
|
> current behavior** and leaves the GPU's alone. `wrap` wins because it
|
|
> is the readable value and the one the reported defect asks for.
|
|
>
|
|
> Corroborating, though not the reason: **Emacs also wraps by
|
|
> default** --- `truncate-lines` is `nil` --- so the choice matches
|
|
> what an Emacs-shaped editor's users expect. See §5a for the part of
|
|
> that comparison which does *not* transfer.
|
|
|
|
|
|
**(a) Horizontal scroll only.** Closes the reported defect --- the line
|
|
becomes readable --- at the lowest risk. `view_left` on the window, an
|
|
offset in the walk, commands to move it, and cursor-follow. Does not
|
|
touch `pos_to_display`'s contract beyond a column shift.
|
|
|
|
**(b) Wrap only.** Matches what many users reach for first, but pays
|
|
the whole row-mapping cost immediately and puts every overlay's
|
|
correctness in the blast radius.
|
|
|
|
**(c) Both, in one lane.** Two mechanisms, one review. Against the
|
|
project's one-feature-one-branch rule in spirit even if it is one
|
|
"feature" in the user's words.
|
|
|
|
**(d) Scroll now (Stage 3), wrap as Stage 4.** Ships readability
|
|
quickly; leaves the mode setting's second value unimplemented for a
|
|
while, which is a discoverability wart --- a setting that names a value
|
|
it does not honor is its own coherence defect.
|
|
|
|
**Revision 2 changes this recommendation.** Revision 1 recommended (a)
|
|
or (d) --- scroll first, wrap later --- on the belief that one renderer
|
|
served both frontends. §1.2 shows that is false, and it undermines the
|
|
recommendation: with the GPU **already wrapping**, shipping
|
|
scroll-only would leave the TUI scrolling while the GUI wraps. The
|
|
frontends would still disagree, and the user would still have no say
|
|
--- which is the actual complaint.
|
|
|
|
**Revised recommendation: (b) wrap first**, as the mode both frontends
|
|
can already almost honor, then scroll as Stage 4.
|
|
|
|
The reasoning inverts cleanly. Wrap is the expensive one in the grid
|
|
renderer and **free in the GPU, where it already happens**; picking it
|
|
first means Stage 3 ends with both frontends doing the same declared
|
|
thing. Scroll is cheap in the grid renderer and **entirely new in the
|
|
GPU** --- which has `scroll_top` but no horizontal counterpart
|
|
anywhere (a search for `scroll_left|hscroll|x_offset` in
|
|
`pmacs-gpu` returns nothing).
|
|
|
|
I still do not recommend (c). But note the cost profile is now the
|
|
mirror image of what revision 1 claimed, so please read that
|
|
recommendation as withdrawn rather than merely amended.
|
|
|
|
### 3.1 The Stage 3 surface, stated
|
|
|
|
Wrap-first leaves an obvious hole: a setting with one legal value is
|
|
not a setting, and the user asked for a choice. Revision 2 left this
|
|
implicit and let a *test* mode stand in for a *product* mode, which is
|
|
how §7's witness ended up describing "scroll/truncate" as if they were
|
|
one thing. They are not, and scroll is deferred.
|
|
|
|
**Decision: Stage 3 ships two values, `wrap` (default) and
|
|
`truncate`.** Scroll is Stage 4.
|
|
|
|
- **`wrap`** --- the default, because it is the only value that leaves
|
|
all text reachable with Stage 3's machinery alone. It is also what
|
|
the GPU does today, so the default is not a behavior change there.
|
|
- **`truncate`** --- one source line per row, clipped at the edge. This
|
|
is exactly what the TUI does today, named and made deliberate, and
|
|
made available in the GPU where it currently is not.
|
|
|
|
**`truncate` is not a placeholder and not a test-only mode, but it is
|
|
incomplete until Stage 4.** The honest description: truncate is the
|
|
*mode*, horizontal scroll is the *navigation* that makes the clipped
|
|
remainder reachable. "Scrollable" in the user's request decomposes into
|
|
exactly those two, and Stage 3 ships the first. Until Stage 4 lands,
|
|
selecting `truncate` means accepting that text past the edge cannot be
|
|
read --- which is why it must not be the default, and why its
|
|
description string has to say so rather than implying a complete
|
|
feature.
|
|
|
|
The alternative --- ship `wrap` alone with no setting and defer the
|
|
whole config surface to Stage 4 --- is defensible and cheaper, and I
|
|
rejected it for one reason: it would leave the TUI's *current*
|
|
truncation reachable only by not-yet-existing configuration, so the
|
|
TUI's existing behavior would become unavailable the moment wrap
|
|
landed. Users reading logs want one line per row. Removing that,
|
|
even temporarily, is a regression dressed as a fix.
|
|
|
|
**If you would rather ship wrap-only and take that regression as
|
|
acceptable for one stage, say so and I will cut `truncate` --- but the
|
|
framing should not pretend the choice is free either way.**
|
|
|
|
---
|
|
|
|
## 4. Q#LL2 --- per-buffer or per-window? **ANSWERED**
|
|
|
|
> **Answered 2026-08-06: buffer-local.** Free in the existing registry,
|
|
> and it matches Emacs, where `truncate-lines` is a buffer-local
|
|
> variable shared by every window on the buffer. A window-local layer
|
|
> would be a **third** config layer built for a need nobody has
|
|
> reported.
|
|
>
|
|
> **How the mode reaches the renderer**, which is the part that needed
|
|
> deciding: `Viewport` carries the **resolved** mode, exactly as it
|
|
> already carries `folds`. The render driver in `editor.rs` holds both
|
|
> the registry and the `BufferId`, resolves once per window per frame,
|
|
> and `TextView` stays config-agnostic --- respecting the registry's
|
|
> "no ambient current buffer" rule (`src/config_registry.rs:837`)
|
|
> rather than working around it.
|
|
>
|
|
> This does put the **mode** per-buffer and the **byte anchor**
|
|
> per-window. That split is correct rather than merely tolerable: the
|
|
> mode is a property of the content, the anchor is a property of the
|
|
> viewport looking at it.
|
|
|
|
|
|
The registry supports **buffer-local** overrides today, for free.
|
|
|
|
But line display is arguably a **window** property: the same buffer in
|
|
a split could reasonably wrap in one pane and truncate in the other.
|
|
|
|
**Scoped to Stage 3, this question is only about the mode**, and
|
|
buffer-local answers it for free. The follow-on --- that `view_left`
|
|
would be unambiguously per-window, since two panes on one buffer must
|
|
scroll independently exactly as they already hold independent
|
|
`view_top`s (`src/desktop.rs:92`) --- **is Stage 4's, not this
|
|
lane's.**
|
|
|
|
Recording it here anyway, because the choice made now constrains it: if
|
|
Stage 3 makes the mode buffer-local and Stage 4 then needs a
|
|
per-window offset, the two halves of one user-facing concept end up
|
|
living at different scopes. That is what Emacs effectively does and it
|
|
is survivable, but it should be a decision rather than a discovery.
|
|
**Q#LL2 asks whether to accept that split now.**
|
|
|
|
---
|
|
|
|
## 5. Q#LL3 --- what does the cursor do?
|
|
|
|
**Under `wrap`, this is a Stage 3 question and it is not optional.**
|
|
`pos_to_display` returns `(source line index, prefix width)`, and
|
|
§2.1 shows both halves stop being true once one source line owns
|
|
several rows. Cursor placement uses that function exclusively, so a
|
|
wrapped buffer with an unrepaired mapping puts the caret on the wrong
|
|
row --- in the grid renderer, which is the half that has to be built.
|
|
Whatever answers it must also serve `overlay_paint`, or diagnostics
|
|
land on the wrong row too.
|
|
|
|
**Under `truncate`, the cursor question is trivial** --- one row per
|
|
line, the existing mapping holds --- **but only until Stage 4.** Moving
|
|
the cursor past the right edge in a truncating view is precisely what
|
|
horizontal scroll exists to handle, and Stage 3 has no answer for it:
|
|
the caret goes off-screen. That is a real, if minor, sharp edge of
|
|
shipping `truncate` without scroll, and §3.1's description string
|
|
should own it.
|
|
|
|
Deferred to Stage 4, recorded here so it is not rediscovered: whether
|
|
explicit horizontal scroll drags the cursor with it (as the wheel does
|
|
vertically) or leaves it for the next motion to snap back. The existing
|
|
"auto-scroll to keep cursor visible" pass that `scroll_window`
|
|
deliberately works around (`src/editor.rs:3624-3628`) is where a
|
|
horizontal analog would live, and its comment already records the
|
|
hazard --- an unconditional snap-back makes explicit scrolling feel
|
|
stuck.
|
|
|
|
---
|
|
|
|
## 5a. Q#LL5 --- agreeing on the mode is not agreeing on the wrap **ANSWERED**
|
|
|
|
> **Answered 2026-08-06: character wrap in BOTH frontends.** The GPU
|
|
> document buffer gets an explicit `Wrap::Glyph` --- its first explicit
|
|
> `set_wrap` --- and the grid walk wraps at the character.
|
|
>
|
|
> **The option analysis changed while deciding, and the change is the
|
|
> reason.** "Teach the grid word wrap" looked like the high-effort,
|
|
> high-parity option. It is not: cosmic-text performs **Unicode line
|
|
> breaking (UAX #14)**, so a grid walk breaking on whitespace would
|
|
> diverge on hyphens, CJK and non-breaking spaces. That buys
|
|
> *approximate* parity, which is worse than honest divergence because
|
|
> it looks unified until it is not. True parity that way requires a
|
|
> UAX #14 dependency.
|
|
>
|
|
> Character wrap in both is the only option that is **true parity,
|
|
> cheap, and Emacs-consistent** (Emacs's default wrap is a character
|
|
> wrap; word wrap is opt-in `visual-line-mode` / `word-wrap`). It also
|
|
> completes the lane's thesis: one declared mode, one declared wrap
|
|
> style, two frontends that agree.
|
|
>
|
|
> **Word wrap becomes a declared third value later** ---
|
|
> `ui.line-wrap = "word"` honored by both frontends --- rather than an
|
|
> inherited library default that only one frontend has. `ConfigKind::Enum`
|
|
> makes adding a choice a clean additive change.
|
|
>
|
|
> **The regression is real and must be stated where users see it, not
|
|
> only here.** GUI users have had word wrap since the GPU frontend
|
|
> existed and never opted into losing it. This belongs in the PR
|
|
> description and the release notes.
|
|
|
|
|
|
Raised by writing §9 and noticing I had put it in "not in scope" as if
|
|
it were a detail. It is not.
|
|
|
|
The lane's thesis is that two frontends should stop disagreeing. But
|
|
choosing `wrap` in both only makes them agree on *whether* to wrap, not
|
|
*how*.
|
|
|
|
The GPU's value is exact and worth naming: `cosmic-text` 0.18.2
|
|
constructs every `Buffer` with **`Wrap::WordOrGlyph`**
|
|
(`buffer.rs:262`) --- word wrap, falling back to glyph wrap for a word
|
|
that cannot fit a line by itself. It is not a trait `Default`; the
|
|
constructor sets it, which is why the GPU document gets it without ever
|
|
asking.
|
|
|
|
The natural grid implementation --- keep walking the cell row and
|
|
continue on the next --- is a plain **character** wrap. Ship both and
|
|
the same buffer at the same width breaks lines in different places in
|
|
the two frontends.
|
|
|
|
Worth noting for whoever writes the tests: the existing
|
|
`wrapped_caret_survives_size_changes` uses `"x".repeat(180)`, a line
|
|
with **no word boundary at all**, so it exercises only
|
|
`WordOrGlyph`'s glyph fallback. It would pass identically under
|
|
`Wrap::Glyph`, and therefore cannot detect the divergence this question
|
|
is about. A prose line is needed to see it.
|
|
|
|
That is a smaller defect than today's truncate-vs-wrap split, and it
|
|
may be an acceptable one. But it is the same *kind* of defect this lane
|
|
exists to close, so it should be decided rather than inherited ---
|
|
which is precisely the mistake §1.2 documents the GPU already making
|
|
once.
|
|
|
|
Options: match `WordOrGlyph` in the grid walk (most work, genuine
|
|
parity); accept character wrap in the grid and document the divergence;
|
|
or set `Wrap::Glyph` on the GPU document buffer so both are character
|
|
wraps (cheapest parity --- one line, and it would be the document
|
|
buffer's *first* explicit `set_wrap` --- but a visible downgrade for
|
|
GUI users who have had word wrap all along without anyone deciding they
|
|
should).
|
|
|
|
No recommendation yet --- I would rather know Q#LL1's answer first,
|
|
since this question only exists if `wrap` ships.
|
|
|
|
---
|
|
|
|
## 5b. Q#LL6 --- the visual-row map is the actual design problem
|
|
|
|
Revision 7 recorded that `pos_to_display` cannot compute a visual row
|
|
from its arguments, and framed that as a signature change. **Review was
|
|
right that this understates it: a signature is not a model.**
|
|
|
|
### 5b.1 The inverse has the same hole, and it is worse
|
|
|
|
`display_to_pos` takes `(&self, buf, coord)` (`src/view.rs:278`) and
|
|
opens by treating the row as a **source line index**
|
|
(`src/text_view.rs:188`):
|
|
|
|
```rust
|
|
let row = coord.row as usize;
|
|
if row >= self.line_count() { return None; }
|
|
```
|
|
|
|
Without width and mode it cannot invert a wrapped visual row at all ---
|
|
and unlike the forward direction, it will not fail loudly. It will
|
|
return a position from the wrong source line.
|
|
|
|
### 5b.2 The vertical stack is in source-line space, end to end
|
|
|
|
- **`Window::view_top` is documented as "First buffer line shown at the
|
|
top of this window's viewport"** (`src/window.rs:374`). Not a visual
|
|
row.
|
|
- **Rendering converts it as a source line:**
|
|
`window.text_view.line_offset(window.view_top)` (`src/editor.rs:4334`).
|
|
- **`move_down` conflates display rows with source lines**
|
|
(`src/editor_core.rs:2145`). It reads `coord.row` from
|
|
`pos_to_display` --- a **display** row --- then treats it as a source
|
|
line: bounding `next_row >= aw.text_view.line_count()` against the
|
|
source line count and passing it through `map.next_visible()`. This
|
|
is correct today only because display row and source line coincide.
|
|
Under `wrap` they diverge, and a one-source-line buffer wrapping to
|
|
two visual rows would refuse to move to the second row.
|
|
|
|
**Revision 9 called `next_visible`'s argument a "visible-line index",
|
|
as though folds introduced a third renumbered space. They do not**
|
|
--- see §5b.3. That mistake matters here specifically: if a reader
|
|
believed folds renumber, the natural fix to `move_down` would be to
|
|
renumber again for wrap, which is precisely the fold-composition
|
|
error §5b.3 exists to prevent.
|
|
|
|
The same source-row assumption runs through `goal_col`
|
|
(`src/window.rs:376`, "sticky display column for vertical motion"),
|
|
`visible_rows` and therefore `cursor.page-down` (`src/window.rs:377`),
|
|
`scroll_window` (`src/editor.rs:3629`), the gutter's line numbers, and
|
|
`overlay_paint`'s row arithmetic (`:178`, `:318`).
|
|
|
|
### 5b.3 It must compose with folds, not replace them
|
|
|
|
This is the constraint that makes it a design item rather than a
|
|
utility --- and getting the existing model right is the first half of
|
|
it, because **revision 9 got it wrong in the direction that would cause
|
|
the very bug this section prevents.**
|
|
|
|
Revision 9 said `VisibleLineMap` "mediates source line to **visible**
|
|
line" and drew:
|
|
|
|
> ~~source line --(folds)--> visible line --(wrap)--> visual row~~
|
|
|
|
**There is no renumbered visible-line space.** `next_visible(line)`
|
|
computes `line + 1` and then jumps past a collapsed component,
|
|
returning a **source-line index** (`src/fold_view.rs:223`); so do
|
|
`prev_visible`, `visible_head_of` and `clamp_view_top`.
|
|
|
|
Folds do not renumber lines. Nor --- a second-order correction, from
|
|
review of revision 10 --- do they **restrict the domain**, which is how
|
|
this paragraph first put it. `clamp_view_top` *deliberately accepts a
|
|
hidden line* and projects it to its visible head; that is the reason it
|
|
exists (`src/fold_view.rs:215`), and `text_view::render` relies on it,
|
|
noting that "a caller that hands us a hidden start still gets its
|
|
head". Calling the domain restricted would imply passing a hidden line
|
|
is a caller error. It is the supported case.
|
|
|
|
**The first step is a source-index-preserving projection onto a
|
|
visible source-line anchor.** Total --- every source line is a legal
|
|
input --- idempotent, and staying inside the same index space
|
|
throughout. (`visible_rows_between` does return a dense count, but that
|
|
is a *distance*, not a coordinate, and nothing indexes with it.)
|
|
|
|
That is the same shape as the coordinate rule in §7, one level up:
|
|
**identity on canonical inputs; otherwise projection to the contract's
|
|
designated canonical representative.**
|
|
|
|
"Designated", not "nearest" --- the distinction is the whole content of
|
|
the rule. A hidden line maps to its **fold head** even when the next
|
|
visible line is closer (`visible_head_of`, not "whichever visible line
|
|
is fewest lines away"), and an interior UTF-8 byte maps to its
|
|
**codepoint start**, which is not necessarily the nearer boundary. Each
|
|
contract names its representative; proximity never selects it.
|
|
|
|
The direction is not shared either, which is why the rule has to be
|
|
stated in terms of designation rather than of going backward.
|
|
`pos_to_display` projects an interior byte **back** to its codepoint
|
|
start, while `display_to_pos` rounds a column landing inside a wide
|
|
character **forward** to the next codepoint boundary
|
|
(`display_to_pos_jumps_over_wide_chars`,
|
|
`display_to_pos_inside_tab_rounds_to_next_codepoint`). Two directions,
|
|
one rule --- each names its own representative.
|
|
|
|
What the two levels genuinely share is the algebra: both are **total**
|
|
(every input is legal) and **idempotent** (projecting twice equals
|
|
projecting once). That is the property the wrap map must also hold, in
|
|
both directions.
|
|
|
|
The accurate model, and the one the wrap map must be built against:
|
|
|
|
> source line *(projected by folds onto a visible source-line anchor)*
|
|
> --(wrap)--> visual row
|
|
|
|
So there are exactly **two** coordinate spaces after this lane, not
|
|
three: source lines, and visual rows. Wrap is the only renumbering
|
|
step, and it consumes a source line the fold map has already projected
|
|
onto a visible anchor.
|
|
|
|
Why the distinction is load-bearing rather than pedantic: a reader who
|
|
believes folds renumber will reach for a second renumbering to layer
|
|
wrap on top, ending with a source-to-visible-to-visual chain in which
|
|
the middle space has no definition and every fold consumer is
|
|
subtly misindexed. A wrap map that instead goes straight from source
|
|
line to visual row *without consulting the fold map* bypasses folding
|
|
and silently breaks it; one that replaces `VisibleLineMap`
|
|
re-implements a merged and reviewed arc. **The map takes a
|
|
fold-vouched source line and returns a visual row**, and both
|
|
directions have to survive that composition.
|
|
|
|
### 5b.4 What Q#LL6 asks --- **ANSWERED**
|
|
|
|
> **Answered 2026-08-06**, in discussion. Item 3 (byte-anchored
|
|
> `view_top`) is above; two structural decisions are in §5b.5 --- **no
|
|
> global map**, and **`DisplayCoord` gains a sub-row rather than
|
|
> redefining `row`**. The resize-restore policy §5b.6 demanded is
|
|
> **dissolved** rather than answered: a byte anchor makes it
|
|
> unnecessary.
|
|
>
|
|
> **Item 1 --- where it lives: `TextView`, no new type, no cache.** It
|
|
> already owns the line offsets and the character walk. Two methods
|
|
> taking width and mode. No cache initially: a viewport is ~50 lines,
|
|
> so that is at most ~50 single-line layouts per frame, the same order
|
|
> as rendering, which already walks every visible line. A per-window
|
|
> `(line, width)` cache is a profiling response, not a design premise.
|
|
>
|
|
> **Item 2 --- signatures.** One `Copy` context value rather than two
|
|
> loose parameters:
|
|
>
|
|
> ```text
|
|
> pos_to_display(buf, pos, ctx) -> Option<DisplayCoord> // ctx: { width, mode }
|
|
> display_to_pos(buf, coord, ctx) -> Option<Position>
|
|
> ```
|
|
>
|
|
> The asymmetry with the `DisplayCoord` decision (§5b.5) is
|
|
> deliberate, and it is the whole audit strategy:
|
|
>
|
|
> - **The input change is breaking on purpose.** A required parameter
|
|
> makes the compiler enumerate every call site, so the §5b.7 audit is
|
|
> mechanical rather than a grep.
|
|
> - **The output change is additive on purpose.** `sub_row` defaults to
|
|
> 0, so a consumer that does not know about wrap stays *correct*, not
|
|
> merely *findable*.
|
|
>
|
|
> Compiler-enforced where enforcement is possible; correct-by-default
|
|
> where it is not.
|
|
|
|
|
|
1. **Where the map lives and who owns it.** Width is a *window*
|
|
property, so a per-window map is the obvious home --- which then
|
|
interacts with Q#LL2's buffer-local mode. A per-window map keyed by
|
|
a buffer-local mode is coherent but should be stated.
|
|
2. **Both directions, explicitly.** Fold-vouched source line to first
|
|
visual row, and visual row back to (source line, row-within-line). §7's
|
|
witnesses test the second; nothing currently tests the first
|
|
because it does not exist.
|
|
3. **What `view_top` becomes --- and revision 8 posed this as a false
|
|
binary.** It offered "stays a source line" or "becomes a visual
|
|
row". **Neither works.**
|
|
|
|
A source line **cannot represent a viewport that begins partway
|
|
down a wrapped line.** A line taller than the viewport must be
|
|
scrollable from its visual row 0 to its visual row 1, and
|
|
"convert the source line" always yields row 0 --- so the second
|
|
half of a tall line would be unreachable by scrolling. That is not
|
|
an edge case; it is the exact situation this lane exists for, since
|
|
the motivating buffers are the ones with very long lines.
|
|
|
|
A bare visual row fails differently --- see §5b.6.
|
|
|
|
**The representation has to be composite: an anchor line plus a
|
|
sub-line offset**, composed with folds (§5b.3). This is load-bearing
|
|
for cursor visibility, wheel scrolling and paging, not a storage
|
|
detail.
|
|
|
|
**ANSWERED 2026-08-06: the sub-line component is a BYTE, not a row
|
|
index.** `view_top` is the byte offset of the first visible
|
|
character (equivalently, anchor line plus byte-within-line).
|
|
|
|
A row index is width-dependent and lossy: narrowing then widening
|
|
then narrowing again does not return the viewport where it started,
|
|
and every width change needs a clamp-or-reset policy. **A byte is
|
|
width-independent**, so resize needs no policy at all --- recompute
|
|
which row that byte falls on at the new width, exactly and
|
|
reversibly. It is the same property that made anchor-line
|
|
persistence safe in §5b.6.
|
|
|
|
It also composes with the established algebra rather than adding a
|
|
rule: an arbitrary byte is not necessarily a row start, so it
|
|
projects onto the row start containing it --- total, idempotent,
|
|
designated representative, exactly as folds and `pos_to_display` do
|
|
(§5b.3).
|
|
|
|
And it satisfies the pinned API contract by construction:
|
|
`view_top()` returns the line containing that byte;
|
|
`set_view_top(n)` sets the byte to line `n`'s start, which *is*
|
|
sub-row zero.
|
|
|
|
**Precedent, found while answering this:** the GPU already carries a
|
|
composite anchor --- `scroll_top` (a source line index into
|
|
`current_line_starts`) plus `code_scroll_residual` (a sub-line
|
|
offset) --- and `normalize_code_scroll` (`pmacs-gpu/src/main.rs:7955`,
|
|
framing Q#F6) already handles reflow pushing that residual across
|
|
source lines, by **renormalizing rather than clamping**. So the
|
|
composite shape is not novel here. The grid can do better than the
|
|
GPU on the sub-component only because it owns its own layout: the
|
|
GPU's residual is in pixels because cosmic-text owns layout there,
|
|
which is why it needs a renormalization loop that byte-anchoring
|
|
does not.
|
|
4. **Whether `truncate` is the identity case.** It should be: under
|
|
`truncate` the map is the identity and every current behavior holds
|
|
unchanged, which is what makes the whole change additive and
|
|
testable against today's suite.
|
|
|
|
### 5b.5 Two structural decisions taken with Q#LL6 --- **ANSWERED**
|
|
|
|
Both from the 2026-08-06 discussion, both load-bearing for cost.
|
|
|
|
**1. There is no global logical-to-visual map, and none is needed.**
|
|
|
|
A materialized "visual row of every line" prefix sum would be `O(N)`
|
|
memory and an `O(N)` rebuild on every width change --- against an M1
|
|
gate that includes `open_100mb_under_200ms`. That is a real perf risk
|
|
and §5b.7's "authoritative map" framing invited it.
|
|
|
|
No **index** is required, because every *positioning* consumer is
|
|
local: rendering walks forward from `view_top` bounded by viewport
|
|
height; `move_down`/`move_up` need one step; paging needs
|
|
viewport-height rows; the wheel needs *n* rows from `view_top`. Nothing
|
|
asks for the absolute visual row of line 40,000, and nothing **indexes**
|
|
by one.
|
|
|
|
**Revision 14 overstated this as "every vertical consumer is local",
|
|
and that is false.** Review of `bd752f2` found the counterexample: the
|
|
scroll indicator needs a **total**. See §5d --- and note the
|
|
distinction that survives, because it is what keeps the cost bounded: a
|
|
*total* is one number, computable lazily and cacheable; a *prefix-sum
|
|
index* is `O(N)` resident storage. Stage 3 needs the former and still
|
|
does not need the latter.
|
|
|
|
So "the map" is two per-line functions --- how many rows this line
|
|
occupies at this width, and which row a given byte falls on --- plus
|
|
incremental walks. **Layout is needed one line at a time.**
|
|
|
|
**2. `DisplayCoord` gains a sub-row; `row` keeps its meaning.**
|
|
|
|
`DisplayCoord { row, col }` (`src/view.rs:110`) is core-internal, 53
|
|
references, 39 inside `text_view.rs`'s own tests --- so roughly eight
|
|
real external uses. Either approach is tractable in size; they are not
|
|
equivalent in risk.
|
|
|
|
**Redefining `row` from source line to absolute visual row would
|
|
silently break every existing consumer** --- `overlay_paint`'s
|
|
`disp.row - view_top` (`:189`, `:321`), `move_down`'s bounds check
|
|
(`src/editor_core.rs:2145`) --- with no compile error, which is the
|
|
exact failure class this framing has been catching all along.
|
|
|
|
Adding a `sub_row` makes it **additive**: `sub_row == 0` under
|
|
`truncate` and for every unwrapped line, so existing consumers stay
|
|
correct by default and wrap-aware ones opt in explicitly. That is what
|
|
bounds the §5b.7 audit: the compiler cannot find these call sites for
|
|
us, so the design has to make the untouched ones *right* rather than
|
|
merely *findable*.
|
|
|
|
### 5b.6 `saveplace` persistence, and why a bare visual row is unsafe
|
|
|
|
`view_top` is written to disk. `saveplace` stores one
|
|
`<cursor> <view_top> <path>` line per file and parses it with
|
|
`^(%d+)%s+(%d+)%s+(.+)$` (`builtin/runtime/saveplace.lua:5`, `:37`),
|
|
restoring via `pmacs.editor.set_view_top` (`:76`). Two properties make
|
|
this sharper than "a format change":
|
|
|
|
- **There is no version marker.** Nothing distinguishes a record
|
|
written before this lane from one written after. Redefine what the
|
|
second integer means and **every existing record is silently
|
|
reinterpreted** --- an old source line 500 becomes visual row 500,
|
|
which in any wrapped buffer is a different place entirely. No error,
|
|
no migration prompt, just a wrong viewport.
|
|
- **The path is the whitespace-split remainder.** So simply appending a
|
|
fourth field is not backward-compatible either: an older pmacs
|
|
reading a newer file parses the new sub-index as the head of the
|
|
path and loses the entry.
|
|
|
|
There is a second, independent problem: **a visual row is
|
|
width-dependent.** Saved at 120 columns and restored at 80, the same
|
|
number denotes a different source location --- and windows legitimately
|
|
change width between sessions, which is precisely what QoL Stage 1 was
|
|
about.
|
|
|
|
**My recommendation, offered as the cheapest correct option rather than
|
|
a decision:** persist only the **anchor line**, never the
|
|
row-within-line offset. Then the stored value keeps its current
|
|
meaning, every existing record stays valid **by construction**, no
|
|
migration or version marker is needed, and the persisted number is
|
|
width-independent again. The cost is bounded and small: reopening a
|
|
file restores to the top of the anchor line rather than partway down
|
|
it --- at most one line's height of drift, and only for files closed
|
|
mid-wrapped-line.
|
|
|
|
**That recommendation is only real if it is an API contract, so state
|
|
it as one.** `saveplace` does not touch a field; it calls public Lua
|
|
(`builtin/runtime/saveplace.lua:60`, `:76`), and those bindings are
|
|
documented today as source lines --- *"view_top(): the active window's
|
|
first visible source line"* and *"set_view_top(line): set the first
|
|
visible source line"* (`src/lua_bindings/mod.rs:13714`). So the
|
|
contract Stage 3 must preserve is:
|
|
|
|
- **`pmacs.editor.view_top()` continues to return the source anchor
|
|
line**, not a visual row, whatever the internal representation
|
|
becomes.
|
|
- **`pmacs.editor.set_view_top(n)` sets that anchor with
|
|
`row_within_line = 0`.**
|
|
|
|
With both held, `saveplace` needs **no change at all** and existing
|
|
records keep working --- the compatibility comes from the API contract,
|
|
not from `saveplace` being careful. Any future call that needs the
|
|
sub-row is a **new** binding, additive, and not what `saveplace`
|
|
writes.
|
|
|
|
This also decides a question Q#LL6 would otherwise leave open: the
|
|
composite `view_top` is an *internal* window representation, and the
|
|
Lua surface exposes only its anchor component. Widening the public
|
|
getter to return a pair would be the change that breaks records
|
|
silently, and it is exactly what "just make `view_top` composite"
|
|
invites if the API is not pinned here.
|
|
|
|
**Q#LL6 must also settle the resize-restore policy**, which the
|
|
composite representation does not escape: when the width changes, a
|
|
row-within-line offset may exceed the line's row count at the new
|
|
width. Clamp to the last row, or reset to 0? This applies to live
|
|
resizes as well as restores, so it is needed regardless of what is
|
|
persisted.
|
|
|
|
### 5b.7 The cost, restated honestly
|
|
|
|
Revision 7 costed this as "~35 `pos_to_display` call sites". **That was
|
|
the wrong unit.** The real work is an audit of *both* mapping APIs plus
|
|
every place that assumes a display row is a source line --- vertical
|
|
motion, paging, wheel scroll, `view_top` handling, gutter numbering,
|
|
overlay placement, and the fold interaction above.
|
|
|
|
Sizing that audit is itself part of Q#LL6, and it is a strong argument
|
|
for `wrap` and `truncate` shipping as one lane with `truncate` as the
|
|
identity case: it gives every one of those consumers a mode in which
|
|
its current behavior is provably unchanged.
|
|
|
|
---
|
|
|
|
## 5c. Q#LL7 --- the GPU needs a wire message, and revision 14 had none
|
|
|
|
**Raised in review of `bd752f2`, and it is a hole in the lane's central
|
|
claim.** §4 resolves `ui.line-wrap` into `Viewport`, which reaches the
|
|
**grid** renderer. The GPU is not a grid consumer (§1.2): it lays out
|
|
locally and ignores `CellDelta`. `BufferSnapshot` carries only CRDT
|
|
bytes (`pmacs-protocol/src/message.rs:777`), and no `InstanceMessage`
|
|
variant expresses a wrap mode.
|
|
|
|
So as framed through revision 14, `ui.line-wrap = "truncate"` would
|
|
change the TUI and **leave the GPU wrapping** --- the two frontends
|
|
still disagreeing, which is the exact defect this lane exists to close.
|
|
Q#LL5's "character wrap in both" is likewise unreachable without a
|
|
wire: setting `Wrap::Glyph` at GPU startup is not the same as honoring
|
|
a mode that can change.
|
|
|
|
### 5c.1 The message
|
|
|
|
**Additive variant, appended after the current final `InstanceMessage`
|
|
variant; `PROTOCOL_VERSION` 21 -> 22; `ADVERTISED_PROTOCOL_VERSION`
|
|
stays 20.** This is the path `FontFacts` took at v17 and the panel
|
|
shapes took at v21, and the constant's own doc reserves moving the
|
|
advertised baseline for changes "that cannot be expressed additively"
|
|
--- this one can.
|
|
|
|
It carries `buffer_id` alongside the mode. **Not optional: the mode is
|
|
buffer-local (§4)**, so "the current mode" is meaningless without
|
|
naming the buffer it belongs to, and the GPU tracks
|
|
`current_buffer_id` already.
|
|
|
|
### 5c.2 Resend semantics --- the part most likely to be got wrong
|
|
|
|
The mode must reach the GPU on **all three** of:
|
|
|
|
1. **Attach**, for the initially-shown buffer, as part of the same
|
|
initial-state burst that establishes font facts. A frontend that
|
|
attaches to an existing session must not have to wait for a change
|
|
to learn the current mode.
|
|
2. **Config change**, via the registry's `on_change` --- for every
|
|
attached frontend showing that buffer.
|
|
3. **Buffer switch.** This is the one a `FontFacts`-shaped design
|
|
misses. Font size is global; **wrap mode is per buffer**, so
|
|
switching from a buffer set to `truncate` to one left at `wrap`
|
|
changes the effective mode with **no config event at all**. A
|
|
design that only listens to `on_change` is silently wrong here, and
|
|
would look correct in every single-buffer test.
|
|
|
|
### 5c.3 GPU behavior on receipt
|
|
|
|
Set `Wrap::Glyph` (mode `wrap`) or `Wrap::None` (mode `truncate`) on
|
|
the **document** buffer --- its first explicit `set_wrap` either way
|
|
(§1.2) --- then reshape and **renormalize the scroll anchor** through
|
|
`normalize_code_scroll` (`pmacs-gpu/src/main.rs:7955`). That path
|
|
already exists for exactly this situation: reflow moving the retained
|
|
residual across source lines. Changing wrap mode reflows the whole
|
|
document, so it is the same event class as a font-size change, and must
|
|
reuse that repair rather than reimplement it.
|
|
|
|
An out-of-range or unknown mode value is **rejected as a whole
|
|
message**, matching `apply_font_facts` rather than clamping --- the
|
|
convention Stage 2 followed (`docs/gui-zoom-framing.md`).
|
|
|
|
### 5c.4 Older frontends
|
|
|
|
A v21-or-older frontend never receives the variant and keeps wrapping.
|
|
That is a **documented divergence**, not a silent one: the guarantee
|
|
"both frontends agree" holds for peers that negotiated v22, and the
|
|
release notes must say so alongside the word-wrap regression (§5a).
|
|
|
|
---
|
|
|
|
## 5d. Q#LL8 --- the scroll indicator, which falsifies "everything is local"
|
|
|
|
**Raised in review of `bd752f2`.** `format_scroll_indicator`
|
|
(`src/editor.rs:5509`) reckons `All`/`Top`/`Bot`/`NN%` from
|
|
`total_lines`, fed in visible-line space (`src/editor.rs:4336`, Arc 6
|
|
Q#FD18) so a collapsed remainder correctly reads `All`.
|
|
|
|
Under `wrap` that is wrong in a way a user sees immediately. **A
|
|
one-line buffer wrapping to fifty screen rows has `total_lines == 1`,
|
|
so the very first branch --- `if total_lines <= 1 { return "All" }` ---
|
|
reports `All` while forty-nine rows sit below the viewport.** The
|
|
indicator claims the whole buffer is on screen when almost none of it
|
|
is.
|
|
|
|
### 5d.1 The contract
|
|
|
|
The indicator is reckoned in **visual rows** whenever the mode is
|
|
`wrap`, and in visible lines under `truncate` --- where the two
|
|
coincide, so `truncate` remains exactly today's behavior, consistent
|
|
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%` --- **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
|
|
|
|
**`All` / `Top` / `Bot` need no aggregate.** Each is a local predicate:
|
|
is the first visual row on screen (`view_top` byte == first visible
|
|
byte), and is the last one (does the forward walk from `view_top` reach
|
|
the buffer end within the viewport)? Both fall out of the render walk
|
|
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:
|
|
|
|
**Fold state must be in the key.** `Viewport.folds` is built **per
|
|
rendered window** (`src/view.rs:146`) and a fold can be collapsed or
|
|
expanded with **no edit, no width change and no mode change** --- so
|
|
all three key components are unchanged while the projection underneath
|
|
them is not. Compute `NN%`, collapse a fold, and the stale total is
|
|
served for the new projection.
|
|
|
|
**Prefer a content-derived key over a maintained one.**
|
|
`VisibleLineMap` is `{ components: Vec<HiddenComponent> }`
|
|
(`src/fold_view.rs:104`) with no revision field, and
|
|
`fold_map_for_window` rebuilds it per call. Two ways to key on it:
|
|
|
|
- A revision counter on the fold registry, bumped by every mutation.
|
|
Cheap to compare, and it carries a *did-you-remember-to-bump* hazard
|
|
on every present and future mutation path --- the same failure shape
|
|
as Q#LL7's buffer-switch trigger.
|
|
- **The projection's own contents.** `components` holds one entry per
|
|
collapsed region, so hashing or comparing it is `O(folds)`, not
|
|
`O(N)` --- negligible per frame, and it **cannot be forgotten**,
|
|
because the key *is* the thing it guards.
|
|
|
|
**Take the second**, for the same reason byte-anchoring beat a row
|
|
index (§5b.4) and an additive `sub_row` beat redefining `row`
|
|
(§5b.5): a key that derives from the state is self-validating, while
|
|
one maintained alongside it is a standing invitation to drift.
|
|
|
|
**And it is the CONTENT width, not the window width.** Wrapping happens
|
|
in the text area, so the gutter is already subtracted --- and the
|
|
gutter's width changes with the line-count digit boundary (9 -> 10,
|
|
99 -> 100), which the GPU's `sync_buffer_dimensions` comment already
|
|
records for its own shaping (`pmacs-gpu/src/main.rs:4338`). Keying on
|
|
window width would serve a stale total across a digit boundary.
|
|
|
|
So: **(buffer generation, content width, mode, fold projection)**.
|
|
|
|
It composes with folds by counting rows only for lines the fold map
|
|
vouches as visible (§5b.3).
|
|
|
|
**It must not become a resident prefix-sum index** --- that is the
|
|
`O(N)` storage §5b.5 rules out, and the distinction is exactly one
|
|
number versus one number per line.
|
|
|
|
**The `open_100mb_under_200ms` gate (M1) constrains this.** Computing
|
|
total visual rows means laying out every line, so it must not happen on
|
|
open, on every frame, or on any path the gate measures --- only on
|
|
first `NN%` paint after an invalidation. If that proves too slow on
|
|
large buffers, the fallback is to report a **byte-based** percentage
|
|
under `wrap` and say so; what is not acceptable is today's silent
|
|
`All`.
|
|
|
|
### 5d.3 The GPU has its own indicator, and revision 15 missed it
|
|
|
|
**Raised in review of `1c9ff6a`.** §5d as written specified only the
|
|
TUI path. `format_scroll_indicator` is **duplicated, not shared** ---
|
|
`src/editor.rs:5509` and `pmacs-gpu/src/main.rs:10114`, each with its
|
|
own tests --- and the GPU calls its copy with
|
|
`self.current_line_starts.len()`, a **source-line** count
|
|
(`pmacs-gpu/src/main.rs:7199`).
|
|
|
|
So a one-line wrapped buffer reports `All` in the GPU too, by an
|
|
entirely independent path. **Stage 3 as framed through revision 15
|
|
would have fixed the indicator in one frontend and left it wrong in the
|
|
other** --- which is this lane's own defect, reproduced by the lane
|
|
meant to close it.
|
|
|
|
The GPU's `visible` argument is wrong under `wrap` for the same reason:
|
|
`estimated_visible_lines(...)` counts **lines**, and visible *rows* is
|
|
what the indicator needs once one line owns several.
|
|
|
|
**Revision 16 said both copies could keep their signature and change
|
|
only what callers pass. That is not sufficient, and review of
|
|
`d6b5285` was right to reject it.** See §5d.5 --- every branch of the
|
|
formatter derives from `total_lines`, which revision 17 removed.
|
|
|
|
**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 formatter cannot express the new contract --- so it is not asked to
|
|
|
|
**The contradiction, stated plainly.** `format_scroll_indicator`
|
|
(`src/editor.rs:5509`) derives **every** branch from `total_lines`:
|
|
|
|
```rust
|
|
if total_lines <= 1 { return "All" }
|
|
if visible >= total_lines { return "All" }
|
|
if view_top == 0 { return "Top" }
|
|
if view_top + visible >= total_lines { return "Bot" }
|
|
let pct = (cursor_row + 1) * 100 / total_lines;
|
|
```
|
|
|
|
Revision 17 removed the total. So:
|
|
|
|
- **Passing byte counts mixes units.** `view_top` and `visible` are
|
|
rows; a byte `total_lines` makes `view_top + visible >= total_lines`
|
|
compare rows against bytes. It would return plausible strings and be
|
|
meaningless.
|
|
- **Passing a fake total restores the bug.** Any stand-in that is
|
|
`<= 1`, or `<= visible`, returns the false `All` this section exists
|
|
to remove.
|
|
|
|
"Local predicates" is therefore not something the retained formatter
|
|
can evaluate --- it has no parameter for them.
|
|
|
|
**Resolution: `truncate` keeps the existing formatter untouched;
|
|
`wrap` gets a new classifier.**
|
|
|
|
This is §5b.5's identity-case strategy applied to the indicator itself,
|
|
and it is stronger than adapting one function to two contracts:
|
|
|
|
- **`truncate` calls `format_scroll_indicator` exactly as today**, with
|
|
the same arguments in the same units. Byte-identical output is
|
|
guaranteed **by construction**, not by a test --- and every existing
|
|
formatter test stays valid unchanged, including the GPU's
|
|
`format_scroll_indicator(0, 10, 1, 0) == "All"` (`:13091`), which
|
|
correctly pins line-space behavior.
|
|
- **`wrap` calls a new classifier** that never sees a row total:
|
|
|
|
```text
|
|
enum ScrollPosition { All, Top, Bot, Percent(u8) }
|
|
|
|
classify(first_visible: bool, // is the buffer's first row on screen?
|
|
last_visible: bool, // is the buffer's last row on screen?
|
|
byte_pos: u64, // cursor byte
|
|
byte_len: u64) -> ScrollPosition
|
|
```
|
|
|
|
`All = first && last`; `Top = first && !last`; `Bot = last && !first`;
|
|
otherwise `Percent` from bytes. **No count of rows enters it**, so the
|
|
unit mixing above is not merely avoided, it is unrepresentable.
|
|
|
|
Attempting one signature for both modes was the actual mistake: the two
|
|
contracts genuinely differ, and a shared four-count signature can only
|
|
serve them by making units implicit --- which is how this contradiction
|
|
arose.
|
|
|
|
### 5d.6 Where the classifier lives --- **ANSWERED: `pmacs-protocol`**
|
|
|
|
`pmacs-gpu` depends on **`pmacs-protocol` only**, never on the `pmacs`
|
|
lib (`pmacs-gpu/Cargo.toml:65`). So `format_scroll_indicator` is
|
|
duplicated **structurally**, not by oversight, and a new classifier
|
|
faces the same fork:
|
|
|
|
- **(a) Duplicate it too.** Matches what is there, adds nothing to any
|
|
crate's remit --- and preserves exactly the condition that produced
|
|
§5d.3's defect, where one copy was fixed and the other was not.
|
|
- **(b) Put it in `pmacs-protocol`.** The only crate both sides
|
|
already share. Agreement becomes **structural rather than
|
|
maintained** --- the principle that chose byte-anchoring, additive
|
|
`sub_row`, and a content-derived cache key.
|
|
|
|
**Answered 2026-08-06: (b), in the narrow form.** `ScrollPosition` and
|
|
the pure `classify(first_visible, last_visible, byte_pos, byte_len)`
|
|
live in `pmacs-protocol`; **string rendering stays in each frontend.**
|
|
|
|
The §16 reading that settles it, and it is a better statement of the
|
|
split than "shared vocabulary": **each frontend computes its own local
|
|
layout facts --- which rows are on screen is a question only it can
|
|
answer --- while the shared crate owns the common semantic decision
|
|
those facts feed.** That is not presentation moving into the protocol
|
|
crate; it is the *decision* being placed where both frontends are
|
|
structurally unable to disagree about it, with the rendering left where
|
|
it belongs.
|
|
|
|
Two properties worth recording because they bound the change:
|
|
|
|
- **No wire message and no protocol-version bump.** `classify` is a
|
|
pure function over values each side already has. Q#LL7's v22 variant
|
|
is unrelated and unaffected.
|
|
- **The §5d.3 defect becomes unrepresentable**, rather than guarded by
|
|
a reviewer noticing the second copy. A frontend cannot classify
|
|
differently, because there is only one classifier.
|
|
|
|
### 5d.7 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.8 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.
|
|
- **The large-file guards of §5d.7**, 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.**
|
|
- **A `truncate` output-identity witness against the retained
|
|
formatter** (§5d.5): same buffer, same viewport, byte-identical
|
|
string. Cheap, and it is the assertion that the identity case is real
|
|
rather than asserted.
|
|
- **Classifier unit-safety**, which is what the old signature could not
|
|
give: `classify` takes two booleans and a byte pair, so a
|
|
rows-versus-bytes comparison is unrepresentable. Witness the four
|
|
outcomes directly --- `first && last` is `All`, `first && !last` is
|
|
`Top`, `last && !first` is `Bot`, neither is `Percent` --- including
|
|
the one-line-wrapped case, where `first && !last` must yield `Top`
|
|
and **not** `All`.
|
|
- 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
|
|
today's output for the same buffer and viewport.
|
|
- A **folded + wrapped** case, since §5b.3's composition applies here
|
|
too and the Q#FD18 contract must survive.
|
|
|
|
---
|
|
|
|
## 6. Q#LL4 --- `editing.fill-column` **ANSWERED**
|
|
|
|
> **Answered 2026-08-06: do not adopt it.** *(Revised 2026-08-07 --- the
|
|
> premise was wrong; the answer was not.)*
|
|
>
|
|
> The 2026-08-06 answer said the setting was orphaned because **its
|
|
> consumer does not exist yet**: no `M-q`, no auto-fill, no reflow
|
|
> command anywhere in the tree. That much is true. But the setting does
|
|
> not exist either --- it is a **test fixture name**, not a definition
|
|
> (§1.1, withdrawn). So the framing of "a setting ahead of its feature"
|
|
> described nothing real, and the `full_grid` comparison it was already
|
|
> walking back was doubly inapt: `full_grid` was a wire flag with a
|
|
> live consumer that ignored it, and this is a string in two
|
|
> `#[cfg(test)]` blocks.
|
|
>
|
|
> **What changed in the deliverables.** One of the two evaporated:
|
|
>
|
|
> - ~~**Sharpen its description.**~~ There is no shipped description.
|
|
> Replaced by: **name the fixtures as fixtures** at
|
|
> `src/config_registry.rs` and `src/lua_bindings/config.rs`, so the
|
|
> next reader does not repeat §1.1. That is the entire remaining cost,
|
|
> and it is a comment.
|
|
> - **Name ours so confusion is impossible: `ui.line-wrap`**,
|
|
> `ConfigKind::Enum { choices: ["wrap", "truncate"] }`, default
|
|
> `"wrap"`. Unchanged, and it never depended on the bad premise ---
|
|
> `editing.*` is buffer-editing behavior, `ui.*` is display; both
|
|
> existing `ui.*` settings carry a `gpu-` prefix to mark
|
|
> frontend-specificity, so its **absence** here is what signals "both
|
|
> frontends". The naming discipline stands on the concept split, which
|
|
> a real `fill-column` would only have made more urgent, not less.
|
|
>
|
|
> **The one place this could have bitten.** Had the premise gone
|
|
> unchecked into implementation, Stage 3 would have shipped an edit to a
|
|
> unit-test fixture believing it was rewording a user-visible setting
|
|
> --- a no-op change with a misleading commit message, and a lane
|
|
> closing on a deliverable it had not delivered.
|
|
|
|
---
|
|
|
|
## 7. Verification sketch
|
|
|
|
Not final --- it depends on Q#LL1.
|
|
|
|
- Unit tests on `text_view::render` at the cell level, **at several
|
|
window widths** --- not "at several offsets", which was the same
|
|
`view_left` assumption in the very first bullet. A line longer than
|
|
`max_cols`; a wide character straddling the wrap/clip column; a tab
|
|
expanded across it (the walk's tab path at `src/text_view.rs:254` has
|
|
its own `col >= max_cols` break, so wrap has to be taught there too,
|
|
not only in the main character path).
|
|
- **Wrapped visual-row mapping**, which is Stage 3's version of this.
|
|
Revision 3 asked for round trips "at non-zero offset" --- that is a
|
|
`view_left` requirement and `view_left` is Stage 4, so the sketch was
|
|
quietly re-importing deferred scope. What `wrap` actually needs
|
|
witnessed:
|
|
- For a source line occupying N visual rows, `pos_to_display` returns
|
|
`{ row: source_line, sub_row, col }` --- the **same `row` it
|
|
returns today**, plus which visual row *within* that line and the
|
|
column within *that* row, rather than the whole prefix width
|
|
(`src/text_view.rs:184`).
|
|
|
|
**Notation matters here and revision 14 got it wrong.** It said
|
|
`pos_to_display` returns "the visual row", which reads as a
|
|
redefinition of `row` --- exactly what §5b.5 forbids. Every example
|
|
below is therefore written as the explicit triple
|
|
`{row, sub_row, col}`; a bare pair anywhere in this section is a
|
|
bug in the document, not a shorthand.
|
|
- `display_to_pos` inverts it: a click on visual row *k* of a wrapped
|
|
line lands in that row's byte range, not the source line's head.
|
|
- Round trip is identity for **every valid cursor boundary** in a
|
|
wrapped line, walked exhaustively rather than sampled --- the line
|
|
is short enough to make that cheap, and sampling is what would miss
|
|
the next case.
|
|
|
|
**"Every position" would be an impossible invariant, and revision 4
|
|
asked for it.** `pos_to_display` accepts a byte offset *inside* a
|
|
multi-byte codepoint and deliberately canonicalizes it: continuation
|
|
bytes outside a complete codepoint are trimmed and the codepoint's
|
|
own column is the answer (`src/text_view.rs:163-167`), while
|
|
`display_to_pos` returns the codepoint **start**
|
|
(`src/text_view.rs:185`, and the existing
|
|
`display_to_pos_jumps_over_wide_chars` /
|
|
`display_to_pos_inside_tab_rounds_to_next_codepoint` pin it). An
|
|
interior byte therefore cannot round-trip to itself today, and
|
|
demanding it would have made the witness unsatisfiable rather than
|
|
discriminating --- the test would have been "fixed" by weakening it,
|
|
which is the failure mode this whole sketch is trying to avoid.
|
|
|
|
The accurate contract is: **identity on boundaries, projection
|
|
elsewhere.** For an interior byte the round trip must land on the
|
|
containing codepoint's start, and applying it twice must equal
|
|
applying it once.
|
|
- **That canonicalization is pre-existing behavior, and this lane
|
|
preserves it unless it says otherwise.** It gets its own witness,
|
|
separate from the wrap tests, so that "wrap changed the interior-byte
|
|
rule" cannot hide inside a wrap failure — or vice versa. If wrap
|
|
turns out to need a different rule at a wrap point that also splits a
|
|
codepoint, that is a deliberate change with its own Q#, not a quiet
|
|
consequence.
|
|
- **The wrap point itself**, which is the case worth designing the
|
|
test around --- and which revision 5 described in a way that
|
|
collapsed two different requirements into one incoherent sentence.
|
|
|
|
Take `abcdef` soft-wrapping after `abc`. Buffer positions are
|
|
`0=a 1=b 2=c 3=d 4=e 5=f`. **Position 3 is a single source position
|
|
with two defensible display coordinates**:
|
|
`{row: L, sub_row: k, col: 3}` --- just past the last glyph of
|
|
visual row *k* of line *L* --- and `{row: L, sub_row: k+1, col: 0}`
|
|
--- just before the first glyph of visual row *k+1* of the **same
|
|
source line**. Note both share `row: L`: the wrap point does not
|
|
cross a source line, which is precisely why redefining `row` would
|
|
have destroyed the information this case turns on. Revision 5 called these "the last
|
|
position on row *k* and the first on row *k+1*" and demanded they
|
|
"not collide". They are the same position. Nothing can be asserted
|
|
about their collision.
|
|
|
|
**Decision: the wrap position belongs to column 0 of row *k+1*.**
|
|
|
|
Revision 6 justified this by calling the alternative "off-grid",
|
|
and parenthetically claimed a hard line end is "within the row".
|
|
**Both halves are wrong.** A hard line ending at exactly `max_cols`
|
|
gets column `max_cols` --- `pos_to_display` sums the prefix width
|
|
and clamps nothing (`src/text_view.rs:184`) --- so it is *also*
|
|
just past the last cell, and that is existing, accepted behavior.
|
|
Off-gridness therefore does not distinguish the two cases at all.
|
|
|
|
Worse, the argument was incoherent on its own terms:
|
|
**`pos_to_display` does not know the grid.** Its signature is
|
|
`(&self, buf, pos)` (`src/view.rs:271`, `src/text_view.rs:156`) ---
|
|
no viewport, no `max_cols`. A function with no notion of the grid
|
|
cannot be reasoned about as producing coordinates "off" it.
|
|
|
|
The rule that actually decides it, and subsumes both cases:
|
|
|
|
> **A position maps to the cell of the glyph that follows it when
|
|
> one exists on some row; otherwise to the column just past the
|
|
> last glyph.**
|
|
|
|
- Soft wrap: position 3 is followed by `d` at
|
|
`{row: L, sub_row: k+1, col: 0}`. A
|
|
following glyph exists, so that is the answer. There is a genuine
|
|
choice here, and this resolves it.
|
|
- Hard line end: no glyph follows on any row, so the coordinate is
|
|
the column just past the last glyph --- `(k, width)`, **including
|
|
`{row: L, sub_row: last, col: max_cols}` when the line fills its
|
|
final visual row exactly.** No choice
|
|
exists, and **this preserves current behavior unchanged**, which
|
|
is the point: the wrap work must not quietly move hard-end
|
|
coordinates.
|
|
|
|
So the two cases differ because one has an alternative and the other
|
|
does not --- not because one is off-grid.
|
|
|
|
**A consequence the earlier revisions missed entirely:** under
|
|
`wrap`, `pos_to_display` **cannot compute a visual row from its
|
|
current arguments.** The wrap width has to reach it. Revision 7
|
|
treated that as a trait signature change and costed it at ~35 call
|
|
sites; **that framing was too narrow, and §5b (Q#LL6) replaces it**
|
|
--- the inverse has the same hole, and `view_top`, vertical motion,
|
|
paging, wheel scroll, gutters and overlays all currently work in
|
|
source-line space. Read §5b before costing this.
|
|
|
|
Consequences to witness, and they are the discriminating ones:
|
|
- `pos_to_display(3)` is `{row: L, sub_row: k+1, col: 0}`, never
|
|
`{row: L, sub_row: k, col: 3}`.
|
|
- **A hard line end that exactly fills the row still maps to
|
|
`{row: L, sub_row: 0, col: max_cols}` with `sub_row` still 0** ---
|
|
a control asserting the wrap work left the
|
|
existing hard-end coordinate alone, since the soft-wrap rule
|
|
superficially resembles a rule that would have moved it.
|
|
- `display_to_pos` on the trailing cells of row *k* --- which exist
|
|
when a wide character forced an early break and left the row's
|
|
last cell blank --- must land on the wrap position, not on the
|
|
last glyph's start. This is the case a naive "clamp to row width"
|
|
gets wrong.
|
|
- **Affinity is explicitly not implemented.** Editors that let
|
|
`End` on row *k* and `Home` on row *k+1* sit visually apart at
|
|
one buffer position carry an upstream/downstream bit to do it.
|
|
Stage 3 carries a single canonical coordinate instead. If that
|
|
distinction is wanted later it is a feature with its own state,
|
|
not a bug in this mapping --- named here so it is a decision
|
|
rather than a discovery.
|
|
- **Distinct adjacent codepoints across the break must map
|
|
distinctly**, which is the requirement revision 5 was reaching for.
|
|
The start of the last codepoint on row *k* (position 2, `c`) and
|
|
the start of the first on row *k+1* (position 3, `d`) are two
|
|
different positions; they must give `{row: L, sub_row: k, col: 2}`
|
|
and `{row: L, sub_row: k+1, col: 0}`, and
|
|
the round trip must return each unchanged.
|
|
- A `truncate` **control** asserting the mapping is unchanged from
|
|
today, so the wrap work cannot silently alter the non-wrapped path.
|
|
- If wrap is in scope: an overlay-placement test with a wrapped line
|
|
above the overlay's row, which is the regression §2.1 predicts.
|
|
- A PTY acceptance test for the user-visible report: with `wrap`, a
|
|
line longer than the terminal is readable in full --- following
|
|
`full_grid_resync_acceptance.rs`'s content-anchored pattern rather
|
|
than any time-based settle. (Not "scrolled past the edge" --- there
|
|
is no scrolling in Stage 3. Revision 2 wrote it that way and was
|
|
describing Stage 4.)
|
|
- **A GPU-side witness that the mode is honored rather than inherited.**
|
|
`wrapped_caret_survives_size_changes` (`pmacs-gpu/src/main.rs:15853`)
|
|
passes today against a wrap nobody configured, so it cannot
|
|
distinguish "honors the setting" from "cosmic-text's default happens
|
|
to match." The discriminating case is the other value: **with the
|
|
mode set to `truncate`, an overlong line must NOT occupy a second
|
|
row.** Without it this lane ships the defect Stage 1 just fixed --- a
|
|
declared setting nothing enforces.
|
|
|
|
This test is a **negative control for mode enforcement**, and it is
|
|
worth being exact about what it does *not* stand for: it is not a
|
|
witness for horizontal scroll, and `truncate` is not the user's
|
|
"scrollable." Scroll is Stage 4 (§3.1). A reader who takes this test
|
|
as evidence that the scroll alternative works would be reading it
|
|
backwards --- it proves only that an explicit non-wrap mode reaches
|
|
the GPU's layout.
|
|
|
|
### 7.1 What the sketch did not anticipate (revision 20)
|
|
|
|
Three witnesses exist that §7 never asked for, each because review or
|
|
implementation found a defect the sketch had no reason to predict.
|
|
Recorded so the gap between sketch and suite is deliberate:
|
|
|
|
- **Rendered-output witnesses**
|
|
(`the_default_actually_wraps_the_painted_text` and its `truncate`
|
|
control, `tests/line_wrap_acceptance.rs`). Every test §7 sketched
|
|
asks a *component* a question. The defect review found lived in the
|
|
**driver**: `src/editor.rs` resolved the mode, recorded it on the
|
|
window, and fed it to coordinate mapping and the indicator --- while
|
|
the `Viewport` literal it built still carried a hard-coded
|
|
`Truncate`. A test constructing its own viewport passes against that.
|
|
These read the grid reconstructed from emitted `CellDelta` spans
|
|
instead.
|
|
- **Two-buffer toggle witnesses** (same file). `ui.line-wrap` is
|
|
buffer-local, so the toggle's failure mode --- reading and writing
|
|
the *global* layer --- is invisible in any single-buffer test, and
|
|
wrong in both directions at once: it leaves a pinned buffer alone
|
|
while silently moving every unpinned one.
|
|
- **GPU scroll-endpoint witnesses** (`pmacs-gpu/src/main.rs`). §5d.6
|
|
settled *what* the classifier consumes; it did not settle how the GPU
|
|
decides `first_visible` / `last_visible`, and the two cheap answers
|
|
are both wrong (`view_range` includes overscan; `scroll_top` ignores
|
|
the sub-line residual). The trailing-empty-line case
|
|
(`an_empty_final_line_still_counts_as_bot`) was found *by* these
|
|
tests, not confirmed by them --- it made every newline-terminated
|
|
file report a percentage instead of `Bot` at the bottom.
|
|
|
|
---
|
|
|
|
## 8. Coherence impact (§20 requirement)
|
|
|
|
- **Scorecard row 11, "Config layering + provenance --- Partial
|
|
(foundation only), 5 settings live in it."** This lane adds
|
|
`ui.line-wrap` against that row --- registry-defined, enum-validated,
|
|
buffer-local, and consumed by both frontends. *(Revision 20: this
|
|
bullet previously also promised to adopt or decline "the orphaned
|
|
`editing.fill-column`". It is not orphaned and not a setting; see
|
|
§1.1, withdrawn. The count in that scorecard row was never affected
|
|
by it either way.)*
|
|
- **Journey step 4, "Understand interface --- Partial."** A line that
|
|
cannot be read in full is a direct hit on this step; the scorecard
|
|
does not currently name it, and should.
|
|
- **§16 Semantic Frontend Architecture --- this is the lane's primary
|
|
coherence citation, per §1.2.** Two frontends currently render the
|
|
same buffer's long lines differently, and neither behavior was
|
|
chosen: the TUI truncates because the cell walk breaks at `max_cols`,
|
|
the GPU wraps because cosmic-text's default was never overridden.
|
|
Stage 3 replaces two accidents with one declared mode.
|
|
- **No new interaction island.** The mode command goes in the ordinary
|
|
command registry and the global keymap. Per Q#Z3's finding in Stage 2,
|
|
`keymap_stack::Scope` carries no frontend identity --- and unlike
|
|
zoom, that is not a constraint here, because the setting is
|
|
frontend-independent by design: both renderers read the same value
|
|
and each honors it in its own layout.
|
|
- **No background-work attribution.** Nothing async.
|
|
|
|
---
|
|
|
|
## 9. Not in scope
|
|
|
|
**Horizontal scroll, in full: `view_left` on the window, the commands
|
|
that move it, and the cursor-follow pass. That is Stage 4** (§3.1,
|
|
§5). Stage 3 ships the `truncate` mode that Stage 4 makes navigable,
|
|
and ships it knowing text past the edge is unreachable in the
|
|
meantime.
|
|
|
|
Reflow/fill commands that *edit* the buffer (`M-q`). Bidi or RTL. A
|
|
minimap. Soft-wrap indicators in the gutter --- worth doing, but they
|
|
are a gutter-arc concern and would need their own Q#.
|