fix: diff_buffers clear-to-end when deleting wide graphemes (#4921)

Fixes #4870  #4717 #3260 #4431 #2718 #4898 #5036

- Fix the chat composer “phantom space” bug that appeared when
backspacing CJK (and other double-width) characters after the composer
got a uniform background in 43b63ccae89c….
- Pull diff_buffers’s clear-to-end logic forward to iterate by display
width, so wide graphemes are counted correctly when computing the
trailing column.
- Keep modifier-aware detection so styled cells are still flushed, and
add a regression test (diff_buffers_clear_to_end_starts_after_wide_char)
that covers the CJK deletion scenario.

---------

Co-authored-by: Josh McKinney <joshka@openai.com>
This commit is contained in:
MomentDerek
2025-10-18 10:03:36 +08:00
committed by GitHub
parent 0e08dd6055
commit 98c6dfa537

View File

@@ -411,28 +411,35 @@ fn diff_buffers<'a>(a: &'a Buffer, b: &'a Buffer) -> Vec<DrawCommand<'a>> {
let next_buffer = &b.content; let next_buffer = &b.content;
let mut updates = vec![]; let mut updates = vec![];
let mut last_nonblank_column = vec![0; a.area.height as usize]; let mut last_nonblank_columns = vec![0; a.area.height as usize];
for y in 0..a.area.height { for y in 0..a.area.height {
let row_start = y as usize * a.area.width as usize; let row_start = y as usize * a.area.width as usize;
let row_end = row_start + a.area.width as usize; let row_end = row_start + a.area.width as usize;
let row = &next_buffer[row_start..row_end]; let row = &next_buffer[row_start..row_end];
let bg = row.last().map(|cell| cell.bg).unwrap_or(Color::Reset); let bg = row.last().map(|cell| cell.bg).unwrap_or(Color::Reset);
let x = row // Scan the row to find the rightmost column that still matters: any non-space glyph,
.iter() // any cell whose bg differs from the rows trailing bg, or any cell with modifiers.
.rposition(|cell| { // Multi-width glyphs extend that region through their full displayed width.
cell.symbol() != " " || cell.bg != bg || cell.modifier != Modifier::empty() // After that point the rest of the row can be cleared with a single ClearToEnd, a perf win
}) // versus emitting multiple space Put commands.
.unwrap_or(0); let mut last_nonblank_column = 0usize;
last_nonblank_column[y as usize] = x as u16; let mut column = 0usize;
if x < (a.area.width as usize).saturating_sub(1) { while column < row.len() {
let (x_abs, y_abs) = a.pos_of(row_start + x + 1); let cell = &row[column];
updates.push(DrawCommand::ClearToEnd { let width = cell.symbol().width();
x: x_abs, if cell.symbol() != " " || cell.bg != bg || cell.modifier != Modifier::empty() {
y: y_abs, last_nonblank_column = column + (width.saturating_sub(1));
bg, }
}); column += width.max(1); // treat zero-width symbols as width 1
} }
if last_nonblank_column + 1 < row.len() {
let (x, y) = a.pos_of(row_start + last_nonblank_column + 1);
updates.push(DrawCommand::ClearToEnd { x, y, bg });
}
last_nonblank_columns[y as usize] = last_nonblank_column as u16;
} }
// Cells invalidated by drawing/replacing preceding multi-width characters: // Cells invalidated by drawing/replacing preceding multi-width characters:
@@ -444,7 +451,7 @@ fn diff_buffers<'a>(a: &'a Buffer, b: &'a Buffer) -> Vec<DrawCommand<'a>> {
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 { if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = a.pos_of(i); let (x, y) = a.pos_of(i);
let row = i / a.area.width as usize; let row = i / a.area.width as usize;
if x <= last_nonblank_column[row] { if x <= last_nonblank_columns[row] {
updates.push(DrawCommand::Put { updates.push(DrawCommand::Put {
x, x,
y, y,
@@ -592,6 +599,7 @@ mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::style::Style;
#[test] #[test]
fn diff_buffers_does_not_emit_clear_to_end_for_full_width_row() { fn diff_buffers_does_not_emit_clear_to_end_for_full_width_row() {
@@ -620,4 +628,22 @@ mod tests {
"expected diff_buffers to update the final cell; commands: {commands:?}", "expected diff_buffers to update the final cell; commands: {commands:?}",
); );
} }
#[test]
fn diff_buffers_clear_to_end_starts_after_wide_char() {
let area = Rect::new(0, 0, 10, 1);
let mut previous = Buffer::empty(area);
let mut next = Buffer::empty(area);
previous.set_string(0, 0, "中文", Style::default());
next.set_string(0, 0, "", Style::default());
let commands = diff_buffers(&previous, &next);
assert!(
commands
.iter()
.any(|command| matches!(command, DrawCommand::ClearToEnd { x: 2, y: 0, .. })),
"expected clear-to-end to start after the remaining wide char; commands: {commands:?}"
);
}
} }