mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-12 02:27:24 +00:00
fix: preserve display width when erasing wide characters (CJK) (#5657)
* fix: preserve display width when erasing wide characters Calculate end-of-line padding from the retained row's display width instead of character indices, ensuring erased wide characters are replaced with enough spaces to cover all their screen columns. Add regression coverage for wide characters, including erasing from the second half of a character. * some additional correctness fixes * rustfmt * add pr * fix flaky test --------- Co-authored-by: Aram Drevekenin <[email protected]>
This commit is contained in:
co-authored by
Aram Drevekenin
parent
208a0da5e9
commit
f39617a4db
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* fix: compress kitty images if the host terminal supports it (https://github.com/zellij-org/zellij/pull/5608)
|
||||
* fix: add websocket heartbeat to webserver to prevernt idle disconnect through proxies (https://github.com/zellij-org/zellij/pull/4633)
|
||||
* fix: memory-usage optimizations and multiple-pane-single-instance-plugins (https://github.com/zellij-org/zellij/pull/5662)
|
||||
* fix: erasing wide CJK character correctness (https://github.com/zellij-org/zellij/pull/5657)
|
||||
|
||||
## [0.45.1] - 2026-08-28
|
||||
* fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522)
|
||||
|
||||
@@ -364,6 +364,18 @@ pub fn send_blocking_command_through_the_cli() {
|
||||
remote_terminal
|
||||
.send_blocking_command_through_the_cli("bash -c 'sleep 2 && exit 42'");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
step_is_complete = true;
|
||||
}
|
||||
step_is_complete
|
||||
},
|
||||
})
|
||||
.add_step(Step {
|
||||
name: "Wait for the typed command to be echoed on a single line",
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
if remote_terminal.snapshot_contains(
|
||||
"$ /usr/src/zellij/zellij run --blocking --floating --close-on-exit -- bash -c 'sleep 2 && exit 42'",
|
||||
) {
|
||||
remote_terminal.send_key(&ENTER);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
step_is_complete = true;
|
||||
|
||||
@@ -6066,16 +6066,18 @@ impl Row {
|
||||
self.width = None;
|
||||
}
|
||||
pub fn position_accounting_for_widechars(&self, x: usize) -> usize {
|
||||
let mut position = x;
|
||||
self.character_index_and_start_column(x).0
|
||||
}
|
||||
fn character_index_and_start_column(&self, x: usize) -> (usize, usize) {
|
||||
let mut column = 0;
|
||||
for (index, terminal_character) in self.columns.iter().enumerate() {
|
||||
if index == position {
|
||||
break;
|
||||
}
|
||||
if terminal_character.width() > 1 {
|
||||
position = position.saturating_sub(terminal_character.width().saturating_sub(1));
|
||||
let character_width = terminal_character.width();
|
||||
if column + character_width > x {
|
||||
return (index, column);
|
||||
}
|
||||
column += character_width;
|
||||
}
|
||||
position
|
||||
(self.columns.len() + (x - column), x)
|
||||
}
|
||||
pub fn replace_and_pad_end(
|
||||
&mut self,
|
||||
@@ -6084,13 +6086,21 @@ impl Row {
|
||||
terminal_character: TerminalCharacter,
|
||||
) {
|
||||
self.osc133_markers.retain(|marker| marker.column <= from);
|
||||
let from_position_accounting_for_widechars = self.position_accounting_for_widechars(from);
|
||||
let to_position_accounting_for_widechars = self.position_accounting_for_widechars(to);
|
||||
let replacement_length = to_position_accounting_for_widechars
|
||||
.saturating_sub(from_position_accounting_for_widechars);
|
||||
let (from_index, _) = self.character_index_and_start_column(from);
|
||||
self.columns.truncate(from_index);
|
||||
let retained_width = self.width();
|
||||
if retained_width < from {
|
||||
let mut gap_fill = EMPTY_TERMINAL_CHARACTER;
|
||||
if let Some(bg_color) = self.bg_color {
|
||||
gap_fill
|
||||
.styles
|
||||
.update(|styles| styles.background = Some(bg_color));
|
||||
}
|
||||
self.columns
|
||||
.extend(std::iter::repeat(gap_fill).take(from - retained_width));
|
||||
}
|
||||
let replacement_length = to.saturating_sub(self.width());
|
||||
let mut replace_with = VecDeque::from(vec![terminal_character; replacement_length]);
|
||||
self.columns
|
||||
.truncate(from_position_accounting_for_widechars);
|
||||
self.columns.append(&mut replace_with);
|
||||
self.width = None;
|
||||
}
|
||||
@@ -6123,17 +6133,17 @@ impl Row {
|
||||
drained_part
|
||||
}
|
||||
pub fn replace_and_pad_beginning(&mut self, to: usize, terminal_character: TerminalCharacter) {
|
||||
let to_position_accounting_for_widechars = self.position_accounting_for_widechars(to);
|
||||
let (to_position_accounting_for_widechars, character_start_column) =
|
||||
self.character_index_and_start_column(to);
|
||||
let width_of_current_character = self
|
||||
.columns
|
||||
.get(to_position_accounting_for_widechars)
|
||||
.map(|character| character.width())
|
||||
.unwrap_or(1);
|
||||
let replaced_end = to + width_of_current_character;
|
||||
let replaced_end = character_start_column + width_of_current_character;
|
||||
self.osc133_markers
|
||||
.retain(|marker| marker.column >= replaced_end);
|
||||
let mut replace_with =
|
||||
VecDeque::from(vec![terminal_character; to + width_of_current_character]);
|
||||
let mut replace_with = VecDeque::from(vec![terminal_character; replaced_end]);
|
||||
if to_position_accounting_for_widechars > self.columns.len() {
|
||||
self.columns.clear();
|
||||
} else if to_position_accounting_for_widechars >= self.columns.len() {
|
||||
|
||||
@@ -19,6 +19,119 @@ use zellij_utils::{
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
#[test]
|
||||
fn erase_line_end_pads_wide_characters_to_display_width() {
|
||||
use crate::panes::grid::Row;
|
||||
use crate::panes::terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER};
|
||||
|
||||
for (content, from, to, expected) in [
|
||||
("ab你cd", 2, 6, "ab "),
|
||||
("ab你cd", 3, 6, "ab "),
|
||||
("你ab好", 3, 6, "你a "),
|
||||
("你好", 0, 6, " "),
|
||||
("ab", 4, 6, "ab "),
|
||||
] {
|
||||
let mut row = Row::from_columns(
|
||||
content
|
||||
.chars()
|
||||
.map(|character| TerminalCharacter::new_styled(character, Default::default()))
|
||||
.collect(),
|
||||
);
|
||||
row.replace_and_pad_end(from, to, EMPTY_TERMINAL_CHARACTER);
|
||||
assert_eq!(row.width_cached(), to, "content={content:?}, from={from}");
|
||||
assert_eq!(
|
||||
row.columns
|
||||
.iter()
|
||||
.map(|cell| cell.character)
|
||||
.collect::<String>(),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn row_from_str(content: &str) -> crate::panes::grid::Row {
|
||||
use crate::panes::terminal_character::TerminalCharacter;
|
||||
crate::panes::grid::Row::from_columns(
|
||||
content
|
||||
.chars()
|
||||
.map(|character| TerminalCharacter::new_styled(character, Default::default()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn row_to_string(row: &crate::panes::grid::Row) -> String {
|
||||
row.columns.iter().map(|cell| cell.character).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erase_line_end_from_second_half_of_wide_character_before_another_wide_character() {
|
||||
use crate::panes::terminal_character::EMPTY_TERMINAL_CHARACTER;
|
||||
|
||||
for (content, from, to, expected) in [
|
||||
("ab你c好", 3, 8, "ab "),
|
||||
("ab你c好", 2, 8, "ab "),
|
||||
("ab你c好", 5, 8, "ab你c "),
|
||||
("你好你好", 3, 8, "你 "),
|
||||
] {
|
||||
let mut row = row_from_str(content);
|
||||
row.replace_and_pad_end(from, to, EMPTY_TERMINAL_CHARACTER);
|
||||
assert_eq!(row.width_cached(), to, "content={content:?}, from={from}");
|
||||
assert_eq!(
|
||||
row_to_string(&row),
|
||||
expected,
|
||||
"content={content:?}, from={from}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erase_line_end_past_row_end_styles_only_cells_from_cursor() {
|
||||
use crate::panes::terminal_character::{AnsiCode, EMPTY_TERMINAL_CHARACTER};
|
||||
|
||||
let mut erase_character = EMPTY_TERMINAL_CHARACTER;
|
||||
erase_character
|
||||
.styles
|
||||
.update(|styles| styles.background = Some(AnsiCode::ColorIndex(1)));
|
||||
|
||||
let mut row = row_from_str("ab");
|
||||
row.replace_and_pad_end(4, 6, erase_character.clone());
|
||||
|
||||
assert_eq!(row.width_cached(), 6);
|
||||
assert_eq!(row_to_string(&row), "ab ");
|
||||
assert!(row
|
||||
.columns
|
||||
.range(2..4)
|
||||
.all(|cell| *cell == EMPTY_TERMINAL_CHARACTER));
|
||||
assert!(row.columns.range(4..6).all(|cell| *cell == erase_character));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erase_line_start_through_wide_character_keeps_row_width() {
|
||||
use crate::panes::terminal_character::EMPTY_TERMINAL_CHARACTER;
|
||||
|
||||
for (content, to, expected) in [
|
||||
("ab你c好", 2, " c好"),
|
||||
("ab你c好", 3, " c好"),
|
||||
("ab你c好", 4, " 好"),
|
||||
("你好你好", 3, " 你好"),
|
||||
("ab", 0, " b"),
|
||||
] {
|
||||
let mut row = row_from_str(content);
|
||||
let original_width = row.width();
|
||||
row.replace_and_pad_beginning(to, EMPTY_TERMINAL_CHARACTER);
|
||||
assert_eq!(
|
||||
row.width_cached(),
|
||||
original_width,
|
||||
"content={content:?}, to={to}"
|
||||
);
|
||||
assert_eq!(
|
||||
row_to_string(&row),
|
||||
expected,
|
||||
"content={content:?}, to={to}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_fixture(fixture_name: &str) -> Vec<u8> {
|
||||
let mut path_to_file = std::path::PathBuf::new();
|
||||
path_to_file.push("../src");
|
||||
|
||||
+3
-4
@@ -1,11 +1,11 @@
|
||||
---
|
||||
source: zellij-server/src/panes/./unit/grid_tests.rs
|
||||
assertion_line: 875
|
||||
assertion_line: 1386
|
||||
expression: "format!(\"{:?}\", grid)"
|
||||
---
|
||||
00 (C): Welcome to fish, the friendly interactive shell
|
||||
01 (C): ⋊> ~/c/zellij on wide-char ⨯ bash 15:35:20
|
||||
02 (C): [aram@green zellij]$ HHHHHH
|
||||
02 (C): [aram@green zellij]$ HHHHHH
|
||||
03 (C):
|
||||
04 (C):
|
||||
05 (C):
|
||||
@@ -23,5 +23,4 @@ expression: "format!(\"{:?}\", grid)"
|
||||
17 (C):
|
||||
18 (C):
|
||||
19 (C):
|
||||
20 (C):
|
||||
|
||||
20 (C):
|
||||
|
||||
Reference in New Issue
Block a user