Skip to content

fix(grid): keep wide characters before the cursor when erasing to end of screen - #5575

Open
dylanpulver wants to merge 1 commit into
zellij-org:mainfrom
dylanpulver:fix-row-truncate-wide-chars
Open

fix(grid): keep wide characters before the cursor when erasing to end of screen#5575
dylanpulver wants to merge 1 commit into
zellij-org:mainfrom
dylanpulver:fix-row-truncate-wide-chars

Conversation

@dylanpulver

@dylanpulver dylanpulver commented Sep 2, 2026

Copy link
Copy Markdown

Row::truncate takes a display column, but converted it to a character index with
x - excess_width_until(x). excess_width_until counts the excess of the first x
characters (columns.iter().take(x)), so as soon as more than one wide character precedes the
column it over-counts and the row is cut shorter than asked. position_accounting_for_widechars,
twenty lines below in the same impl, is the converter that gets this right, and
replace_and_pad_end already uses it.

The visible effect is through clear_all_after_cursor (CSI 0 J, which shells emit constantly):
with the cursor anywhere past a second wide character, text before the cursor is deleted.
Measured on main (af38660), first row after the sequence, 20-column grid:

input expected on main
日本 then CUP col 3, ESC[0J (empty)
a日本 then CUP col 4, ESC[0J a日 a
ab then CUP col 3, ESC[0J ab ab

Narrow-only rows are unaffected, which is why this has stayed invisible.

Nothing in the suite could see it: every wide-character test in grid_tests.rs
(wide_characters, wide_characters_line_wrap, bash_delete_wide_characters,
fish_wide_characters_override_clock) is an assert_snapshot! of a fixture replay, and the
hand-written ED tests are all ASCII. truncate's formula and those fixtures landed in the same
commit, 44d67de1 (#535, 2021-05-26). Reverting only grid.rs to main fails the one new test
and nothing else; all 1577 lib tests pass with the fix and no snapshot needed regenerating.

Worth saying: the naive version — truncate_position = x, ignoring wide characters — also passes
the new test. It fails five existing ones (delete_char_in_middle_of_line_with_widechar,
insert_character_in_line_with_wide_character, and three search_tests), so the suite pins
over-truncation while being blind to under-truncation. position_accounting_for_widechars is what
satisfies both directions.

Out of scope: the other excess_width_until(x) call, in add_character_at's Ordering::Less arm,
is not affected — there x is at or past the row's display width, so the two readings coincide.
#5240 touches replace_and_pad_end, not truncate; no overlap.

cargo test -p zellij-server --lib (1577 passed), cargo fmt --all --check clean. cargo clippy
at the pinned 1.95.0 already fails on main in output/mod.rs and plugins/unit/plugin_tests.rs;
nothing in the changed files is new.

Found by reading Row's three display-column/character-index converters against each other, not
from normal use. Written with AI assistance (Claude Opus 5, claude-opus-5); every number above
came from running the built test binary.

… of screen

Row::truncate takes a display column but converted it to a character
index with `x - excess_width_until(x)`. excess_width_until counts the
excess of the first `x` *characters*, so once more than one wide
character precedes the column it over-counts and the row is cut short.

Erasing to the end of the screen (CSI 0 J) with the cursor anywhere
after a second wide character therefore deleted text before the cursor.

Convert with position_accounting_for_widechars, the converter the rest
of Row already uses.
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