diff options
author | Aram Drevekenin <aram@poor.dev> | 2021-05-26 18:05:43 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-26 18:05:43 +0200 |
commit | 44d67de187640b0064716050f082be2f330d8df5 (patch) | |
tree | 7ffebf7aa616a4ae05a579fd601bad62b8915ace /zellij-server | |
parent | f55805d653d86e4ac5c1386a980d929b09cb18e5 (diff) |
fix(compatibility): support wide characters (#535)
* fix(compatibility): support wide characters
* style(fmt): rustfmt
* style(fmt): make clippy happy
Diffstat (limited to 'zellij-server')
23 files changed, 554 insertions, 117 deletions
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index ac347db0e..14bdbcd16 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1,3 +1,5 @@ +use unicode_width::UnicodeWidthChar; + use std::{ cmp::Ordering, collections::{BTreeSet, VecDeque}, @@ -251,9 +253,6 @@ impl Grid { colors, } } - pub fn contains_widechar(&self) -> bool { - self.viewport.iter().any(|c| c.contains_widechar()) - } pub fn advance_to_next_tabstop(&mut self, styles: CharacterStyles) { let mut next_tabstop = None; for tabstop in self.horizontal_tabstops.iter() { @@ -421,12 +420,12 @@ impl Grid { canonical_line_parts.push(Row::new().canonical()); } while !canonical_line.columns.is_empty() { - let next_wrap = if canonical_line.len() > new_columns { - canonical_line.columns.drain(..new_columns) + let next_wrap = if canonical_line.width() > new_columns { + canonical_line.drain_until(new_columns) } else { - canonical_line.columns.drain(..) + canonical_line.columns.drain(..).collect() }; - let row = Row::from_columns(next_wrap.collect()); + let row = Row::from_columns(next_wrap); // if there are no more parts, this row is canonical as long as it originally // was canonical (it might not have been for example if it's the first row in // the viewport, and the actual canonical row is above it in the scrollback) @@ -522,9 +521,13 @@ impl Grid { .viewport .iter() .map(|r| { + let excess_width = r.excess_width(); let mut line: Vec<TerminalCharacter> = r.columns.iter().copied().collect(); // pad line - line.resize(self.width, EMPTY_TERMINAL_CHARACTER); + line.resize( + self.width.saturating_sub(excess_width), + EMPTY_TERMINAL_CHARACTER, + ); line }) .collect(); @@ -657,7 +660,11 @@ impl Grid { } } } - pub fn add_character_at_cursor_position(&mut self, terminal_character: TerminalCharacter) { + pub fn add_character_at_cursor_position( + &mut self, + terminal_character: TerminalCharacter, + max_width: usize, + ) { match self.viewport.get_mut(self.cursor.y) { Some(row) => { if self.insert_mode { @@ -665,6 +672,7 @@ impl Grid { } else { row.add_character_at(terminal_character, self.cursor.x); } + row.truncate(max_width); } None => { // pad lines until cursor if they do not exist @@ -678,6 +686,7 @@ impl Grid { } pub fn add_character(&mut self, terminal_character: TerminalCharacter) { // TODO: try to separate adding characters from moving the cursors in this function + let character_width = terminal_character.width; if self.cursor.x >= self.width { if self.disable_linewrap { return; @@ -703,8 +712,8 @@ impl Grid { } } } - self.add_character_at_cursor_position(terminal_character); - self.move_cursor_forward_until_edge(1); + self.add_character_at_cursor_position(terminal_character, self.width); + self.move_cursor_forward_until_edge(character_width); } pub fn move_cursor_forward_until_edge(&mut self, count: usize) { let count_to_move = std::cmp::min(count, self.width - (self.cursor.x)); @@ -714,19 +723,11 @@ impl Grid { self.viewport .get_mut(self.cursor.y) .unwrap() - .truncate(self.cursor.x); - if self.cursor.x < self.width - 1 { - let mut characters_to_append = vec![replace_with; self.width - self.cursor.x]; - self.viewport - .get_mut(self.cursor.y) - .unwrap() - .append(&mut characters_to_append); - } + .replace_and_pad_end(self.cursor.x, self.width, replace_with); } pub fn replace_characters_in_line_before_cursor(&mut self, replace_with: TerminalCharacter) { - let line_part = vec![replace_with; self.cursor.x + 1]; let row = self.viewport.get_mut(self.cursor.y).unwrap(); - row.replace_beginning_with(line_part); + row.replace_and_pad_beginning(self.cursor.x, replace_with); } pub fn clear_all_after_cursor(&mut self, replace_with: TerminalCharacter) { if let Some(cursor_row) = self.viewport.get_mut(self.cursor.y) { @@ -946,13 +947,15 @@ impl Grid { empty_character.styles = empty_char_style; let current_row = self.viewport.get_mut(self.cursor.y).unwrap(); for _ in 0..count { - current_row.delete_character(self.cursor.x); + let deleted_character = current_row.delete_and_return_character(self.cursor.x); + let excess_width = deleted_character + .map(|terminal_character| terminal_character.width) + .unwrap_or(0) + .saturating_sub(1); + for _ in 0..excess_width { + current_row.insert_character_at(empty_character, self.cursor.x); + } } - let mut empty_space_to_append = vec![empty_character; count]; - self.viewport - .get_mut(self.cursor.y) - .unwrap() - .append(&mut empty_space_to_append); } fn add_newline(&mut self) { self.add_canonical_line(); @@ -988,6 +991,7 @@ impl Perform for Grid { // is a little faster let terminal_character = TerminalCharacter { character: c, + width: c.width().unwrap_or(0), styles: self.cursor.pending_styles, }; self.set_preceding_character(terminal_character); @@ -1628,20 +1632,47 @@ impl Row { self.is_canonical = true; self } - pub fn contains_widechar(&self) -> bool { - self.columns.iter().any(|c| c.is_widechar()) + pub fn width(&self) -> usize { + let mut width = 0; + for terminal_character in self.columns.iter() { + width += terminal_character.width; + } + width + } + pub fn excess_width(&self) -> usize { + let mut acc = 0; + for terminal_character in self.columns.iter() { + if terminal_character.width > 1 { + acc += terminal_character.width - 1; + } + } + acc + } + pub fn excess_width_until(&self, x: usize) -> usize { + let mut acc = 0; + for terminal_character in self.columns.iter().take(x) { + if terminal_character.width > 1 { + acc += terminal_character.width - 1; + } + } + acc } pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { - match self.columns.len().cmp(&x) { - Ordering::Equal => self.columns.push(terminal_character), + match self.width().cmp(&x) { + Ordering::Equal => { + self.columns.push(terminal_character); + } Ordering::Less => { - self.columns.resize(x, EMPTY_TERMINAL_CHARACTER); + let width_offset = self.excess_width_until(x); + self.columns + .resize(x.saturating_sub(width_offset), EMPTY_TERMINAL_CHARACTER); self.columns.push(terminal_character); } Ordering::Greater => { + let width_offset = self.excess_width_until(x); // this is much more performant than remove/insert self.columns.push(terminal_character); - self.columns.swap_remove(x); + self.columns.swap_remove(x.saturating_sub(width_offset)); } } } @@ -1661,7 +1692,11 @@ impl Row { // this is much more performant than remove/insert if x < self.columns.len() { self.columns.push(terminal_character); - self.columns.swap_remove(x); + let character = self.columns.swap_remove(x); + let excess_width = character.width.saturating_sub(1); + for _ in 0..excess_width { + self.columns.insert(x, terminal_character); + } } } pub fn replace_columns(&mut self, columns: Vec<TerminalCharacter>) { @@ -1671,12 +1706,77 @@ impl Row { self.columns.push(terminal_character); } pub fn truncate(&mut self, x: usize) { - self.columns.truncate(x); + let width_offset = self.excess_width_until(x); + let truncate_position = x.saturating_sub(width_offset); + if truncate_position < self.columns.len() { + self.columns.truncate(truncate_position); + } + } + pub fn position_accounting_for_widechars(&self, x: usize) -> usize { + let mut position = x; + 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)); + } + } + position + } + pub fn replace_and_pad_end( + &mut self, + from: usize, + to: usize, + terminal_character: TerminalCharacter, + ) { + 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 mut replace_with = vec![terminal_character; replacement_length]; + self.columns + .truncate(from_position_accounting_for_widechars); + self.columns.append(&mut replace_with); } pub fn append(&mut self, to_append: &mut Vec<TerminalCharacter>) { self.columns.append(to_append); } + pub fn drain_until(&mut self, x: usize) -> Vec<TerminalCharacter> { + let mut drained_part: Vec<TerminalCharacter> = vec![]; + let mut drained_part_len = 0; + loop { + if self.columns.is_empty() { + break; + } + let next_character_len = self.columns.get(0).unwrap().width; + if drained_part_len + next_character_len <= x { + drained_part.push(self.columns.remove(0)); + drained_part_len += next_character_len; + } else { + break; + } + } + 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 width_of_current_character = self + .columns + .get(to_position_accounting_for_widechars) + .map(|character| character.width) + .unwrap_or(1); + let mut replace_with = vec![terminal_character; to + width_of_current_character]; + if to_position_accounting_for_widechars > self.columns.len() { + self.columns.clear(); + } else { + drop(self.columns.drain(0..=to_position_accounting_for_widechars)); + } + replace_with.append(&mut self.columns); + self.columns = replace_with; + } pub fn replace_beginning_with(&mut self, mut line_part: Vec<TerminalCharacter>) { + // this assumes line_part has no wide characters if line_part.len() > self.columns.len() { self.columns.clear(); } else { @@ -1691,20 +1791,25 @@ impl Row { pub fn is_empty(&self) -> bool { self.columns.is_empty() } - pub fn delete_character(&mut self, x: usize) { + pub fn delete_and_return_character(&mut self, x: usize) -> Option<TerminalCharacter> { if x < self.columns.len() { - self.columns.remove(x); + Some(self.columns.remove(x)) + } else { + None } } pub fn split_to_rows_of_length(&mut self, max_row_length: usize) -> Vec<Row> { let mut parts: Vec<Row> = vec![]; let mut current_part: Vec<TerminalCharacter> = vec![]; + let mut current_part_len = 0; for character in self.columns.drain(..) { - if current_part.len() == max_row_length { + if current_part_len + character.width > max_row_length { parts.push(Row::from_columns(current_part)); current_part = vec![]; + current_part_len = 0; } current_part.push(character); + current_part_len += character.width; } if !current_part.is_empty() { parts.push(Row::from_columns(current_part)) diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 5549a388f..e5c5e15de 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -99,9 +99,6 @@ impl Pane for PluginPane { fn position_and_size_override(&self) -> Option<PositionAndSize> { self.position_and_size_override } - fn contains_widechar(&self) -> bool { - false - } fn should_render(&self) -> bool { self.should_render } diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index e6489eb42..ececb2a73 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -1,5 +1,3 @@ -use unicode_width::UnicodeWidthChar; - use std::convert::TryFrom; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::{Index, IndexMut}; @@ -8,6 +6,7 @@ use zellij_utils::vte::ParamsIter; pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { character: ' ', + width: 1, styles: CharacterStyles { foreground: Some(AnsiCode::Reset), background: Some(AnsiCode::Reset), @@ -750,6 +749,7 @@ impl Cursor { pub struct TerminalCharacter { pub character: char, pub styles: CharacterStyles, + pub width: usize, } impl ::std::fmt::Debug for TerminalCharacter { @@ -758,16 +758,6 @@ impl ::std::fmt::Debug for TerminalCharacter { } } -impl TerminalCharacter { - pub fn width(&self) -> usize { - self.character.width().unwrap_or(0) - } - - pub fn is_widechar(&self) -> bool { - self.width() > 1 - } -} - fn parse_sgr_color(params: &mut dyn Iterator<Item = u16>) -> Option<AnsiCode> { match params.next() { Some(2) => Some(AnsiCode::RgbCode(( diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 7e45c77bb..32910618d 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -123,9 +123,6 @@ impl Pane for TerminalPane { fn position_and_size_override(&self) -> Option<PositionAndSize> { self.position_and_size_override } - fn contains_widechar(&self) -> bool { - self.grid.contains_widechar() - } fn should_render(&self) -> bool { self.grid.should_render } diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index 1a1d7f5cc..bce67e062 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -396,3 +396,162 @@ fn terminal_reports() { } assert_snapshot!(format!("{:?}", grid.pending_messages_to_pty)); } + +#[test] +fn wide_characters() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "wide_characters"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn wide_characters_line_wrap() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "wide_characters_line_wrap"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn fish_wide_characters_override_clock() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "fish_wide_characters_override_clock"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn bash_delete_wide_characters() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "bash_delete_wide_characters"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn delete_wide_characters_before_cursor() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "delete_wide_characters_before_cursor"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn delete_wide_characters_before_cursor_when_cursor_is_on_wide_character() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "delete_wide_characters_before_cursor_when_cursor_is_on_wide_character"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn delete_wide_character_under_cursor() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); + let fixture_name = "delete_wide_character_under_cursor"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_snapshot!(format!("{:?}", grid)); +} + +#[test] +fn replace_wide_character_under_cursor() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new(21, 104, Palette::default()); |