Skip to content

fix(output): clear stale continuation cell when wide grapheme shifts to new column (13047)#5

Open
beorn wants to merge 1 commit into
mainfrom
fix/wide-emoji-continuation-13047
Open

fix(output): clear stale continuation cell when wide grapheme shifts to new column (13047)#5
beorn wants to merge 1 commit into
mainfrom
fix/wide-emoji-continuation-13047

Conversation

@beorn
Copy link
Copy Markdown
Owner

@beorn beorn commented May 19, 2026

Summary

  • Fixes wide-emoji continuation-cell stale-content bug (📋t 1, 📄i 4 symptoms).
  • Narrows the "skip continuation when main was just emitted" optimization in changesToAnsi to only skip when prev[x-1] was already wide. When prev[x-1] was narrow (wide grapheme is NEW at x-1), emit an explicit CUP+space at x to overwrite the stale glyph — terminals with wcwidth disagreement do not auto-claim x.
  • Threads prev through changesToAnsi as an optional parameter; incremental paths pass it, bare callers opt out.
  • Three regression fixtures: 80-col flex-end group shift (dense emission path), single-column shift (scatter path), narrow→wide at same column.

Root cause

diffBuffers already marks the new continuation cell as dirty (packed metadata differs from prev — continuation flag mismatch). The bug was in changesToAnsi, which skipped the continuation entry under the assumption that the terminal auto-claimed x after the wide-char main at x-1. That holds when prev[x-1] was already wide (cursor advances 2 columns past a wide grapheme), but not when prev[x-1] was narrow — under wcwidth disagreement on emoji like 📋/📄, the terminal advances only 1 column and the stale glyph at x remains visible.

Test plan

  • bun vitest run --project vendor vendor/silvery/tests/features/wide-emoji-continuation.test.tsx — 3/3 green (was 2/3 red — Fixtures B + C failed without the fix)
  • bun vitest run --project vendor vendor/silvery/tests/features/ — 228+ pass (pre-existing flake in layout-churn-leaks-pixels exists on main and is independent of this fix)
  • SILVERY_STRICT=2 bun vitest run --project vendor vendor/silvery/tests/features/wide-emoji-continuation.test.tsx — green under canary + incremental invariants

Bead: @km/silvery/13047-wide-emoji-continuation-cell-stale

…to new column (13047)

When a wide emoji (📋, 📄) shifts to a new column across renders, the
continuation cell at the new column-plus-one lands at a position that held
narrow content in the prior frame. diffBuffers correctly marks both cells
as dirty, but changesToAnsi's "skip continuation when main was just emitted"
optimization dropped the continuation entry — leaving the terminal's stale
narrow glyph visible next to the new wide grapheme ("📋t 1", "📄i 4").

The skip is correct only when prev[x-1] was also wide — then the terminal
auto-advances past the wide grapheme into the continuation column. When
prev[x-1] was narrow (the wide grapheme is NEW at x-1), terminals with
wcwidth disagreement do not auto-claim x, so the stale narrow glyph stays.

Fix: narrow the skip. When lastEmittedX === x - 1 (continuation matches a
just-emitted main), check prevBuffer.isCellWide(x-1, y) — if prev[x-1] was
narrow, emit CUP + space at x to overwrite the stale glyph before skipping.
Threads prev through changesToAnsi as an optional parameter (incremental
paths pass it; bare callers continue to opt out).

Repro: 3 fixtures in tests/features/wide-emoji-continuation.test.tsx —
80-col flex-end group shift (dense path), single-column shift (scatter
path), and direct narrow→wide-at-same-column transition. All red before
fix, green after.

Bead: @km/silvery/13047-wide-emoji-continuation-cell-stale
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant