pmacs/docs
Levi Neuwirth bec8fc9aae
feat(view): horizontal scroll, text and decorations together
Stage 4 of the QoL arc, framing revision 4 (approved). Under
`truncate`, text past the right edge was UNREACHABLE; moving the cursor
now brings it into view. Automatic only — no commands, no bindings, no
new interaction island (Q#HS2).

THE CONTRACT. `view_left` is an unsnapped per-window display column
(Q#HS7(a)), and each line derives its own effective edge during the
walk it already performs from column 0. Starting at 0 is not laziness:
tab expansion depends on the absolute column from the line start, so a
walk beginning at the edge would put tab stops in the wrong place. The
walk stays line-absolute and only the emit translates.

Where the edge bisects a wide glyph on a given line (Q#HS7(c′)), its
trailing cell paints a styled BLANK rather than a `Continuation` — that
glyph means "the cell before me is a wide glyph's head", and here that
cell is off-screen, so emitting it would name a cell nobody painted.
The mapping designates that cell to the glyph's START byte, which keeps
`byte_at_place` total over visible cells and makes the character the
user scrolled toward clickable. Tabs keep FORWARD rounding (Q#HS7(c″))
— preserved, not chosen.

DECORATIONS TRAVEL WITH THE TEXT. The first version of this commit
translated the base glyph walk and nothing else, which split the frame
in half: at `view_left = 10` a glyph from source column 10 painted at
screen column 0 while its syntax style, diagnostic underline, search
wash and `BufferStyleOverlay` span painted at screen column 10 — or
vanished. Decorations drifting off the characters they describe,
silently, and only once a window had been scrolled.

Every such site carried the same two lines (`start_col.min(max_cols)`,
`end_col.min(max_cols)`), correct only while the left edge was pinned
at zero. `Viewport::visible_cols` is now the one rule all FIVE adopters
share — syntax/LSP styling, diagnostic underlines, search washes,
`BufferStyleOverlay`, and the selection painter — so a future decorator
inherits the translation instead of re-deriving it. It also subsumes
the old `end_col <= start_col` guard rather than sitting beside it.
`StyleSpanOverlay` and `VirtualCellOverlay` are deliberately untouched:
they are documented as viewport-relative, so translating them would be
the mirror defect.

The selection painter was nearly a sixth site with its own copy of the
rule, which I justified by a width it supposedly needed and the
viewport lacked. That was FALSE — the render viewport's
`cell_size.cols` is already `rect.size.cols - gutter_w` and its origin
already sits past the gutter. It now takes that same viewport and drops
its `rect`/`gutter_w` parameters entirely. A canonical rule with one
honest exception is not canonical.

The selection painter had the same defect with a worse failure mode: it
asked `pos_to_display` through the LIVE context, which returns `None`
for a position left of the edge, so a selection beginning off-screen
and reaching into view took `continue` and painted NOTHING. That is the
common shape, not an edge case — select rightward from column 0 past
the window width and the view scrolls with the cursor.

TWO THINGS THE TESTS FOUND, both in `pos_to_display`. My framing note
said a caret sits between characters so never lands inside a glyph;
true for the caret, false for the DESIGNATION direction — the glyph's
start byte must map to its visible trailing cell, so `screen_col` needs
the straddle rule and not a bare subtraction. And the `take == 0` early
return short-circuited the translation entirely, so byte 0 looked
visible at every offset.

`view_left` is inert under `wrap` BY CONSTRUCTION —
`LayoutCtx::effective_left` and `Viewport::left_edge` return 0 while
wrapping — rather than by every caller remembering.

Persisted per leaf at DESKTOP_VERSION 1 (Q#HS5) with both approval
conditions: `#[serde(default)]` and a literal v1 JSON fixture omitting
the field, hand-written because a generated one would gain the field
and prove nothing.

Also: `view_left: window.view_left` in the render viewport, not a
literal 0. My mechanical fill put 0 there and it is EXACTLY the
`aa3cd4d` defect — coordinates and the indicator following the scroll
while the painter stays pinned at column 0.

BITE, per clause. Forcing `bisected = false` fails the multi-line
straddle witness; dropping the backward designation fails the
round-trip witness; removing `#[serde(default)]` fails the v1 fixture;
pinning `visible_cols` to an absolute clamp fails all three decorator
witnesses; restoring the selection painter's live-context lookup fails
the off-screen-start selection witness. Each alone. And with selection
now reading the shared helper, pinning `visible_cols` to an absolute
clamp fails the selection witnesses TOO — which is the check that the
duplication is really gone rather than merely reworded.

One unrelated red, logged as R7 in ci-red-signatures.md — the first
this session with a COMPLETE signature, so a matchable row rather than
a U note. `pmacs-gpu`'s managed-retry attach hit a BrokenPipe once
under full-sweep load and did not reproduce (6 isolated runs plus a
clean 113-target sweep). Per the rerun rule that is intermittence only,
and the row explicitly does not claim harmlessness. Not attributed to
this lane: Stage 4 touches no `pmacs-gpu` file and adds no wire
surface.

Gates: fmt; clippy --workspace --all-targets -D warnings, both
configurations; `cargo test --workspace --no-fail-fast -- --skip
basedpyright` 113 targets exit 0, and the same with --features crdt,
113 targets exit 0; git diff --check. No protocol change, so no version
bump and no protocol-bump matrix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-07 22:43:17 +02:00
..
active-work.md feat(view): horizontal scroll, text and decorations together 2026-08-07 22:43:17 +02:00
agent-handoff.md docs: Stage 4 revision 4 — the tab rule was already there 2026-08-07 21:23:23 +02:00
auto-indent-framing.md docs: sync auto-indent review metadata 2026-07-10 15:57:38 -04:00
auto-pairing-framing.md test(edit): PR #110 round 3 — pin raw-byte predicate posture and top-level sets guard 2026-07-12 16:20:01 +01:00
autosave-recovery-framing.md fix(persistence): adopt clears the old owner's skip cache; failing sweeps are loud 2026-07-09 12:06:49 -04:00
bottom-panel-framing.md docs: bottom panel framing (revision 4, amended) 2026-07-24 13:00:46 -04:00
bottom-panel-stage2-framing.md docs(bottom-panel): record framing revision 7 2026-07-29 22:37:41 -04:00
bottom-panel-stage3-framing.md review round 1: the crdt suite Stage 3 missed, and two stale explanations 2026-08-04 12:08:28 +02:00
ci-crdt-coverage-framing.md review round 2: assert the CRDT capability defaults, not their round-trip 2026-08-01 11:37:19 -04:00
ci-red-signatures.md feat(view): horizontal scroll, text and decorations together 2026-08-07 22:43:17 +02:00
comment-toggle-framing.md docs: comment-toggle framing (Arc 2) 2026-07-09 22:35:35 -04:00
compile-mode-framing.md fix(compile): make overlay teardown atomic 2026-07-14 10:57:14 +01:00
config-registry-framing.md fix(config): reject wrongly-typed spec fields; make trim-on-save buffer-aware 2026-07-21 18:29:18 -04:00
context-menu-framing.md docs: right-click context menu framing + as-built (Q#CM) 2026-06-27 22:19:00 -04:00
cua-typeover-undo-framing.md CUA type-over is a single undo step (Q#U1) 2026-06-15 20:00:53 -04:00
desktop-save-framing.md fix(persistence): reliable daemon gate, unarm, per-pane after-load 2026-07-08 22:27:24 -04:00
dired-framing.md docs: record dired Stage 1 (#165) as landed 2026-07-25 17:39:27 -04:00
dired-stage2-framing.md docs: dired Stage 2 framing rev 9 2026-07-28 21:09:11 -04:00
discovery-stage1-command-family-framing.md docs(discovery): finalize Stage 1 framing review 2026-07-31 18:28:35 -04:00
distribution-stage1-framing.md docs(release): a tag before the merge does nothing, silently 2026-08-01 14:46:27 -04:00
editing-conveniences-framing.md fix(edit): PR #111 round 1 — scalar-valid UTF-8, per-word capitalize, trim error reporting 2026-07-12 16:34:18 +01:00
folding-framing.md docs(folding): framing rev 5 — record approval + keybinding decision 2026-07-23 09:59:37 -04:00
folding-stage2-framing.md docs(folding): Stage 2 framing rev 4 — address review round 3 2026-07-23 17:25:55 -04:00
full-grid-resync-framing.md docs: the framing said "awaiting approval" after being approved 2026-08-06 15:22:03 +02:00
generated-buffer-immutability-framing.md merge: integrate main after PR 188 lands 2026-07-29 12:28:25 -04:00
gpu-attach-robustness-framing.md fix(gpu): F-008 fail-fast must actually tear down the session, not just flag it 2026-07-03 17:14:14 -04:00
gpu-chord-forwarding-framing.md docs: GPU general chord forwarding framing + as-built (Q#GC) 2026-07-03 10:05:55 -04:00
gpu-golden-harness-framing.md docs: GPU headless render harness framing + as-built (Q#GH) 2026-07-03 12:15:19 -04:00
gpu-initial-target-framing.md test(journey): pin the destination commit, and land the doc updates 2026-07-26 17:25:22 -04:00
gpu-invocation-framing.md Record second GPU invocation review closure 2026-07-23 12:40:03 -04:00
gpu-minibuffer-framing.md docs: GPU minibuffer framing + as-built (Q#MB) 2026-06-30 21:03:32 -04:00
gpu-set-font-framing.md fix(font): close stage-2 GPU behavioral findings 2026-07-18 15:29:25 +01:00
gpu-terminal-input-framing.md docs: measure the CI coverage gap and record the vterm as-framed audit 2026-07-25 17:17:23 -04:00
gui-zoom-framing.md docs(zoom): the ties both round up, they do not oppose 2026-08-06 17:50:03 +02:00
horizontal-scroll-framing.md feat(view): horizontal scroll, text and decorations together 2026-08-07 22:43:17 +02:00
in-buffer-completion-framing.md docs(completion): phase-2 as-built notes (Q#C6 narrowing, window routing, v15 riders) 2026-07-07 19:57:10 -04:00
in-buffer-search-framing.md docs: consolidate in-buffer search framing (substring + regex) 2026-06-27 15:03:31 -04:00
inline-math-framing.md docs: record the inline-math slice (#158) as landed 2026-07-25 18:16:31 -04:00
inline-math-slice-framing.md fix(math): review round 3 — mapping bug, whitespace defect, real MATH gaps 2026-07-24 20:19:40 -04:00
journey-stage1a-framing.md feat(compile): make building discoverable — journey step 9 2026-07-30 21:13:12 -04:00
journey-stage1b1-compile-defaults-framing.md docs(journey): framing revision 2 — close review round 1 2026-07-30 18:40:34 -04:00
journey-stage1b2-lsp-guidance-framing.md docs(journey): framing revision 4 — close review round 3 2026-07-30 21:52:15 -04:00
journey-stage1b3-welcome-framing.md docs(journey): framing revision 4 — close review round 3 2026-07-31 14:55:57 -04:00
json-yaml-framing.md docs: absorption pass at c5f7501#209 landed, and six weeks of drift 2026-08-01 13:56:23 -04:00
keybindings.md docs(coherence): §5 substrate-without-surface -> Partial 2026-07-31 19:17:22 -04:00
kill-ring-framing.md docs: kill-ring framing (Arc 2), rev 3 2026-07-09 20:10:39 -04:00
latex-grammar-math-substrate-framing.md docs(inline-math): commit the parent inline-math framing 2026-07-23 14:06:26 -04:00
lean4-mode-framing.md fix(lean4): count fan-outs where a claim cannot skip the count 2026-07-26 17:15:24 -04:00
locals-query-processing-framing.md docs: record landed locals-query processing 2026-07-22 13:30:07 -04:00
long-lines-framing.md test: the report itself, at a real PTY — and §1.1 was wrong 2026-08-07 19:37:46 +02:00
lsp-panels-framing.md docs(panels): correct as-built accuracy — position encoding landed, refresh drifted refs 2026-07-08 14:29:23 -04:00
lua-bindings-split-framing.md refactor(lua): re-export install_* wiring fns to preserve the public API 2026-07-06 11:57:56 -04:00
macos-ci-signal-integrity-framing.md docs: record Stage 2 in the framing, and fence R3 off from R2's retirement 2026-08-05 13:51:40 +02:00
mcp-for-package-authors.md
mode-system-wiring-framing.md fix: harden mode acceptance startup 2026-07-22 08:18:18 -04:00
modeline-detection-framing.md docs: record landed modeline detection 2026-07-22 11:30:45 -04:00
multi-language-injections-framing.md fix(injections): PR #122 round 2 — sibling precedence, observable cap, docs 2026-07-15 12:07:21 +01:00
package-author-guide.md feat(statusline): add composable modeline segments at protocol v18 2026-07-21 12:01:25 -04:00
package-manager-hardening-framing.md fix(packages): F-005 must also guard the frozen/lockfile plan path 2026-07-03 19:05:08 -04:00
packages.md
persistence-framing.md docs: persistence framing (Arc 3) 2026-07-08 17:33:25 -04:00
pmacs-gpu-design.md test(font): wire acceptance suite + protocol/design docs (items 2-8, 15) 2026-07-18 11:46:44 +01:00
pmacs-gpu-diagnostic-parity-framing.md framing: pmacs-gpu diagnostic parity — quad squiggles, minimap marks 2026-06-12 10:00:06 -04:00
pmacs-gpu-mouse-deferred-framing.md framing: mouse deferred set — triple-click, Shift-click, minimap jump, edge auto-scroll 2026-06-12 12:12:18 -04:00
pmacs-gpu-mouse-framing.md
pmacs-gpu-perline-reshape-framing.md session R — per-line incremental reshape (typing latency floor) 2026-06-10 15:01:51 -04:00
pmacs-gpu-phase-a-audit.md
pmacs-gpu-quad-backgrounds-audit.md
pmacs-gpu-quad-backgrounds-framing.md
pmacs-gpu-scroll-framing.md
pmacs-gpu-status-band-framing.md framing: GPU status band — local freshness + v8 StatusFacts 2026-06-12 13:45:12 -04:00
pmacs-gpu-wavy-squiggles-framing.md pmacs-gpu: wavy diagnostic squiggles (Q#W1) 2026-06-15 18:41:56 -04:00
process-signal-diagnostic-completeness-framing.md docs(process): stop overclaiming the pre-kill sample; drop a dead comment 2026-07-30 14:41:50 -04:00
process-signal-tolerance-framing.md docs: frame the PTY terminate diagnostic (revision 4) 2026-07-25 21:12:51 -04:00
process-teardown-stdin-deadlock-framing.md fix(process): arm the setsid dependency, correct the orphan account 2026-07-29 22:54:06 -04:00
project.md
query-replace-framing.md fix(query-replace): pin the session to its origin buffer (wrong-buffer guard) 2026-07-08 17:07:08 -04:00
reap-ledger-silent-failures-framing.md fix(process): scope the escalation claim to ticks, and pin the boundary 2026-07-30 20:41:03 -04:00
repository-audit-2026-07-03.md docs(features): document the Lua feature matrix; drop unreachable compile_error idea (F-002) 2026-07-03 19:54:35 -04:00
resource-op-delete-guard-framing.md fix(lsp): close review gaps in workspace edit reporting 2026-07-29 12:29:50 -04:00
roadmap-2026-07.md docs: record Vterm Stage 3 landing 2026-07-22 19:54:14 -04:00
semantic-frontend-protocol.md feat(statusline): add composable modeline segments at protocol v18 2026-07-21 12:01:25 -04:00
side-quest-backlog.md docs: absorption pass at c5f7501#209 landed, and six weeks of drift 2026-08-01 13:56:23 -04:00
statusline-segments-framing.md fix(statusline): harden narrow-band review edges 2026-07-21 12:43:22 -04:00
tab-width-parity-framing.md docs: link tab-width parity PR 2026-07-22 15:06:10 -04:00
terminal-config-and-copy-mode-framing.md docs: correct the non-adopter attribution and two stale anchors 2026-07-26 16:45:12 -04:00
test-ambient-config-isolation-framing.md docs(framing): record the deliberate departure from the §7 branch plan 2026-07-31 19:46:33 -04:00
theme-faces-framing.md perf(themes): PR #120 round 5 -- O(1) frozen counts via store totals 2026-07-15 11:19:03 +01:00
tree-primitive-framing.md fix(listview): ids must also be unique and not NaN 2026-08-05 23:06:25 +02:00
ux-arc-framing.md docs(ux): as-built for sub-arc 3 + arc close (relative/hybrid, protocol v14) 2026-07-07 10:03:32 -04:00
vterm-framing.md Merge main, and fold #173's a37 findings into the CI-coverage lane 2026-07-26 09:29:42 -04:00
web-grammars-html-css-framing.md docs+test(web): name and pin the @attribute retro-paint (review round 3) 2026-07-23 16:11:47 -04:00