diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-03-02 09:15:19 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-02 09:15:19 +0100 |
| commit | ee78f7b8f69ec62af10ce9d59e3de27dfcfc4e73 (patch) | |
| tree | 9fdad1e1fc3a61339eff5f670d435c148ac9a29b | |
| parent | fix: translate plugin paths in api (add missing methods) (#4767) (diff) | |
| download | sl0ppij-ee78f7b8f69ec62af10ce9d59e3de27dfcfc4e73.tar.gz sl0ppij-ee78f7b8f69ec62af10ce9d59e3de27dfcfc4e73.tar.xz sl0ppij-ee78f7b8f69ec62af10ce9d59e3de27dfcfc4e73.zip | |
performance: de-bottleneck resizing/line-wrapping (#4778)
* vec -> vecdeque
* use cached width when resizing
* reuse optimized split_to_rows_of_length function
* accept vecdeque directly - remove unsafe
* add pr
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | zellij-server/src/panes/grid.rs | 125 | ||||
| -rw-r--r-- | zellij-server/src/panes/hyperlink_tracker.rs | 8 | ||||
| -rw-r--r-- | zellij-server/src/panes/search.rs | 11 | ||||
| -rw-r--r-- | zellij-server/src/panes/unit/grid_tests.rs | 2 |
5 files changed, 70 insertions, 77 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b6402e..4d711249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: allow specifying "close_on_exit" when opening panes in place from keybinding or cli (https://github.com/zellij-org/zellij/pull/4760) * fix: respect cursor blink and shape in the web client even when terminal attempts to override it (https://github.com/zellij-org/zellij/pull/4762) * feat: allow cli/plugins/keybindings to atomically show/hide floating panes rather than just toggle (https://github.com/zellij-org/zellij/pull/4764) +* fix: improve line-wrapping/resize performance (https://github.com/zellij-org/zellij/pull/4778) ## [0.43.1] - 2025-08-08 diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index d7cb3576..04563b2a 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -41,7 +41,7 @@ use crate::panes::Selection; use crate::ui::components::UiComponentParser; use zellij_utils::data::PaneContents; -fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> { +fn get_top_non_canonical_rows(rows: &mut VecDeque<Row>) -> Vec<Row> { let mut index_of_last_non_canonical_row = None; for (i, row) in rows.iter().enumerate() { if row.is_canonical { @@ -74,7 +74,7 @@ fn get_lines_above_bottom_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> V } } -fn get_viewport_bottom_canonical_row_and_wraps(viewport: &mut Vec<Row>) -> Vec<Row> { +fn get_viewport_bottom_canonical_row_and_wraps(viewport: &mut VecDeque<Row>) -> Vec<Row> { let mut index_of_last_non_canonical_row = None; for (i, row) in viewport.iter().enumerate().rev() { index_of_last_non_canonical_row = Some(i); @@ -90,7 +90,7 @@ fn get_viewport_bottom_canonical_row_and_wraps(viewport: &mut Vec<Row>) -> Vec<R } } -fn get_top_canonical_row_and_wraps(rows: &mut Vec<Row>) -> Vec<Row> { +fn get_top_canonical_row_and_wraps(rows: &mut VecDeque<Row>) -> Vec<Row> { let mut index_of_first_non_canonical_row = None; let mut end_index_of_first_canonical_line = None; for (i, row) in rows.iter().enumerate() { @@ -119,7 +119,7 @@ fn get_top_canonical_row_and_wraps(rows: &mut Vec<Row>) -> Vec<Row> { fn transfer_rows_from_lines_above_to_viewport( lines_above: &mut VecDeque<Row>, - viewport: &mut Vec<Row>, + viewport: &mut VecDeque<Row>, sixel_grid: &mut SixelGrid, count: usize, max_viewport_width: usize, @@ -147,7 +147,7 @@ fn transfer_rows_from_lines_above_to_viewport( None => break, // no more rows } } - viewport.insert(0, next_lines.pop().unwrap()); + viewport.push_front(next_lines.pop().unwrap()); lines_added_to_viewport += 1; } if !next_lines.is_empty() { @@ -161,7 +161,7 @@ fn transfer_rows_from_lines_above_to_viewport( } fn transfer_rows_from_viewport_to_lines_above( - viewport: &mut Vec<Row>, + viewport: &mut VecDeque<Row>, lines_above: &mut VecDeque<Row>, sixel_grid: &mut SixelGrid, count: usize, @@ -189,8 +189,8 @@ fn transfer_rows_from_viewport_to_lines_above( } fn transfer_rows_from_lines_below_to_viewport( - lines_below: &mut Vec<Row>, - viewport: &mut Vec<Row>, + lines_below: &mut VecDeque<Row>, + viewport: &mut VecDeque<Row>, count: usize, max_viewport_width: usize, ) { @@ -218,13 +218,13 @@ fn transfer_rows_from_lines_below_to_viewport( } for _ in 0..(lines_pulled_from_viewport + 1) { if !next_lines.is_empty() { - viewport.push(next_lines.remove(0)); + viewport.push_back(next_lines.remove(0)); } } } if !next_lines.is_empty() { let excess_row = Row::from_rows(next_lines); - lines_below.insert(0, excess_row); + lines_below.push_front(excess_row); } } @@ -352,8 +352,8 @@ fn utf8_mouse_coordinates(column: usize, line: isize) -> Vec<u8> { #[derive(Clone)] pub struct Grid { pub(crate) lines_above: VecDeque<Row>, - pub(crate) viewport: Vec<Row>, - pub(crate) lines_below: Vec<Row>, + pub(crate) viewport: VecDeque<Row>, + pub(crate) lines_below: VecDeque<Row>, horizontal_tabstops: BTreeSet<usize>, alternate_screen_state: Option<AlternateScreenState>, cursor: Cursor, @@ -474,7 +474,7 @@ impl Default for MouseTracking { impl Debug for Grid { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut buffer: Vec<Row> = self.viewport.clone(); + let mut buffer: Vec<Row> = Vec::from(self.viewport.clone()); // pad buffer for _ in buffer.len()..self.height { buffer.push(Row::new().canonical()); @@ -543,8 +543,8 @@ impl Grid { let _ = SCROLL_BUFFER_SIZE.set(DEFAULT_SCROLL_BUFFER_SIZE); Grid { lines_above: VecDeque::new(), - viewport: vec![Row::new().canonical()], - lines_below: vec![], + viewport: VecDeque::from(vec![Row::new().canonical()]), + lines_below: VecDeque::new(), horizontal_tabstops: create_horizontal_tabstops(columns), cursor: Cursor::new(0, 0, styled_underlines), cursor_is_hidden: false, @@ -648,10 +648,10 @@ impl Grid { ) } - fn recalculate_scrollback_buffer_count(&self) -> usize { + fn recalculate_scrollback_buffer_count(&mut self) -> usize { let mut scrollback_buffer_count = 0; - for row in &self.lines_above { - let row_width = row.width(); + for row in &mut self.lines_above { + let row_width = row.width_cached(); // rows in lines_above are unwrapped, so we need to account for that if row_width > self.width { scrollback_buffer_count += calculate_row_display_height(row_width, self.width); @@ -754,8 +754,8 @@ impl Grid { let mut found_something = false; if !self.lines_above.is_empty() && self.viewport.len() == self.height { self.is_scrolled = true; - let line_to_push_down = self.viewport.pop().unwrap(); - self.lines_below.insert(0, line_to_push_down); + let line_to_push_down = self.viewport.pop_back().unwrap(); + self.lines_below.push_front(line_to_push_down); let transferred_rows_height = transfer_rows_from_lines_above_to_viewport( &mut self.lines_above, @@ -783,7 +783,7 @@ impl Grid { && self.viewport.len() == self.height && !self.viewport.is_empty() { - let mut line_to_push_up = self.viewport.remove(0); + let mut line_to_push_up = self.viewport.pop_front().unwrap(); self.scrollback_buffer_lines += calculate_row_display_height(line_to_push_up.width(), self.width); @@ -927,32 +927,16 @@ impl Grid { let mut new_viewport_rows = vec![]; for mut canonical_line in viewport_canonical_lines { - let mut canonical_line_parts: Vec<Row> = vec![]; - if canonical_line.columns.is_empty() { - canonical_line_parts.push(Row::new().canonical()); - } - while !canonical_line.columns.is_empty() { - let next_wrap = canonical_line.drain_until(new_columns); - // If the next character is wider than the grid (i.e. there is nothing in - // `next_wrap`, then just abort the resizing - if next_wrap.is_empty() { - break; - } - 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) - let row = if canonical_line_parts.is_empty() && canonical_line.is_canonical { - row.canonical() - } else { - row - }; - canonical_line_parts.push(row); + let mut canonical_line_parts = canonical_line.split_to_rows_of_length(new_columns); + // If a character is wider than the grid, split_to_rows_of_length returns an empty + // vec — skip the line, matching the old `break` behavior + if canonical_line_parts.is_empty() { + continue; } new_viewport_rows.append(&mut canonical_line_parts); } - self.viewport = new_viewport_rows; + self.viewport = VecDeque::from(new_viewport_rows); let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index) + (cursor_index_in_canonical_line / new_columns); @@ -1124,7 +1108,7 @@ impl Grid { y_offset: usize, ) -> (Vec<CharacterChunk>, Vec<SixelImageChunk>) { let changed_character_chunks = self.output_buffer.changed_chunks_in_viewport( - &self.viewport, + self.viewport.make_contiguous(), self.width, self.height, x_offset, @@ -1168,10 +1152,12 @@ impl Grid { .serialize(to_serialize.as_slice(), self.osc8_hyperlinks, None) .ok() }, - None => self - .output_buffer - .serialize(&self.viewport, self.osc8_hyperlinks, None) - .ok(), + None => { + let viewport_vec: Vec<Row> = self.viewport.iter().cloned().collect(); + self.output_buffer + .serialize(&viewport_vec, self.osc8_hyperlinks, None) + .ok() + }, } } pub fn render( @@ -1363,7 +1349,8 @@ impl Grid { for _ in 0..self.height { let columns = VecDeque::from(vec![character.clone(); self.width]); - self.viewport.push(Row::from_columns(columns).canonical()); + self.viewport + .push_back(Row::from_columns(columns).canonical()); } self.output_buffer.update_all_lines(); } @@ -1391,10 +1378,10 @@ impl Grid { if self.alternate_screen_state.is_none() { self.transfer_rows_to_lines_above(1); } else if !self.viewport.is_empty() { - self.viewport.remove(0); + self.viewport.pop_front(); } - self.viewport.push(Row::new().canonical()); + self.viewport.push_back(Row::new().canonical()); self.selection.move_up(1); } else { if scroll_region_top < self.viewport.len() { @@ -1404,7 +1391,7 @@ impl Grid { self.viewport .insert(scroll_region_bottom, Row::new().canonical()); } else { - self.viewport.push(Row::new().canonical()); + self.viewport.push_back(Row::new().canonical()); } } self.output_buffer.update_all_lines(); // TODO: only update scroll region lines @@ -1415,7 +1402,7 @@ impl Grid { // but for some reason this breaks rendering in various situations // it needs to be investigated and fixed let new_row = Row::new().canonical(); - self.viewport.push(new_row); + self.viewport.push_back(new_row); } if self.cursor.y == self.height.saturating_sub(1) { self.output_buffer.update_all_lines(); @@ -1477,10 +1464,10 @@ impl Grid { None => { // pad lines until cursor if they do not exist for _ in self.viewport.len()..self.cursor.y { - self.viewport.push(Row::new().canonical()); + self.viewport.push_back(Row::new().canonical()); } self.viewport - .push(Row::new().with_character(terminal_character).canonical()); + .push_back(Row::new().with_character(terminal_character).canonical()); self.output_buffer.update_line(self.cursor.y); }, } @@ -1570,17 +1557,17 @@ impl Grid { self.transfer_rows_to_lines_above(1); self.hyperlink_tracker.offset_cursor_lines(1); } else if !self.viewport.is_empty() { - self.viewport.remove(0); + self.viewport.pop_front(); } let wrapped_row = Row::new(); - self.viewport.push(wrapped_row); + self.viewport.push_back(wrapped_row); self.selection.move_up(1); self.output_buffer.update_all_lines(); } else { self.cursor.y += 1; if self.viewport.len() <= self.cursor.y { let line_wrapped_row = Row::new(); - self.viewport.push(line_wrapped_row); + self.viewport.push_back(line_wrapped_row); self.output_buffer.update_line(self.cursor.y); } else if let Some(current_line) = self.viewport.get_mut(self.cursor.y) { current_line.is_canonical = false; @@ -1606,7 +1593,8 @@ impl Grid { fn pad_lines_until(&mut self, position: usize, pad_character: TerminalCharacter) { for _ in self.viewport.len()..=position { let columns = VecDeque::from(vec![pad_character.clone(); self.width]); - self.viewport.push(Row::from_columns(columns).canonical()); + self.viewport + .push_back(Row::from_columns(columns).canonical()); self.output_buffer.update_line(self.viewport.len() - 1); } } @@ -1720,7 +1708,8 @@ impl Grid { self.viewport .insert(scroll_region_bottom, Row::from_columns(columns).canonical()); } else { - self.viewport.push(Row::from_columns(columns).canonical()); + self.viewport + .push_back(Row::from_columns(columns).canonical()); } } self.output_buffer.update_all_lines(); // TODO: move accurately @@ -1806,9 +1795,9 @@ impl Grid { } pub fn reset_terminal_state(&mut self) { self.lines_above = VecDeque::new(); - self.lines_below = vec![]; + self.lines_below = VecDeque::new(); self.is_scrolled = false; - self.viewport = vec![Row::new().canonical()]; + self.viewport = VecDeque::from(vec![Row::new().canonical()]); self.alternate_screen_state = None; self.cursor_key_mode = false; self.clear_viewport_before_rendering = true; @@ -3048,8 +3037,10 @@ impl Perform for Grid { // enter alternate buffer let current_lines_above = std::mem::replace(&mut self.lines_above, VecDeque::new()); - let current_viewport = - std::mem::replace(&mut self.viewport, vec![Row::new().canonical()]); + let current_viewport = std::mem::replace( + &mut self.viewport, + VecDeque::from(vec![Row::new().canonical()]), + ); let current_cursor = std::mem::replace( &mut self.cursor, Cursor::new(0, 0, self.styled_underlines), @@ -3543,7 +3534,7 @@ impl Perform for Grid { #[derive(Clone)] pub struct AlternateScreenState { lines_above: VecDeque<Row>, - viewport: Vec<Row>, + viewport: VecDeque<Row>, cursor: Cursor, sixel_grid: SixelGrid, supports_kitty_keyboard_protocol: bool, @@ -3551,7 +3542,7 @@ pub struct AlternateScreenState { impl AlternateScreenState { pub fn new( lines_above: VecDeque<Row>, - viewport: Vec<Row>, + viewport: VecDeque<Row>, cursor: Cursor, sixel_grid: SixelGrid, supports_kitty_keyboard_protocol: bool, @@ -3567,7 +3558,7 @@ impl AlternateScreenState { pub fn apply_contents_to( &mut self, lines_above: &mut VecDeque<Row>, - viewport: &mut Vec<Row>, + viewport: &mut VecDeque<Row>, cursor: &mut Cursor, sixel_grid: &mut SixelGrid, supports_kitty_keyboard_protocol: &mut bool, diff --git a/zellij-server/src/panes/hyperlink_tracker.rs b/zellij-server/src/panes/hyperlink_tracker.rs index 12f6843d..56bcfcdb 100644 --- a/zellij-server/src/panes/hyperlink_tracker.rs +++ b/zellij-server/src/panes/hyperlink_tracker.rs @@ -47,7 +47,7 @@ impl HyperlinkTracker { &mut self, ch: char, cursor: &Cursor, - viewport: &mut Vec<Row>, + viewport: &mut VecDeque<Row>, lines_above: &mut VecDeque<Row>, link_handler: &mut LinkHandler, ) { @@ -144,7 +144,7 @@ impl HyperlinkTracker { fn finalize_and_apply( &mut self, - viewport: &mut Vec<Row>, + viewport: &mut VecDeque<Row>, lines_above: &mut VecDeque<Row>, link_handler: &mut LinkHandler, ) { @@ -183,7 +183,7 @@ impl HyperlinkTracker { fn apply_hyperlink_to_grid( &self, link: &DetectedLink, - viewport: &mut Vec<Row>, + viewport: &mut VecDeque<Row>, lines_above: &mut VecDeque<Row>, link_handler: &mut LinkHandler, ) { @@ -319,7 +319,7 @@ mod tests { } } - fn create_test_viewport(rows: usize, cols: usize) -> Vec<Row> { + fn create_test_viewport(rows: usize, cols: usize) -> VecDeque<Row> { (0..rows).map(|_| create_test_row(cols)).collect() } diff --git a/zellij-server/src/panes/search.rs b/zellij-server/src/panes/search.rs index 5c410ad6..80a2bf5f 100644 --- a/zellij-server/src/panes/search.rs +++ b/zellij-server/src/panes/search.rs @@ -2,6 +2,7 @@ use super::Selection; use crate::panes::terminal_character::TerminalCharacter; use crate::panes::{Grid, Row}; use std::borrow::Cow; +use std::collections::VecDeque; use std::fmt::Debug; use zellij_utils::input::actions::SearchDirection; use zellij_utils::position::Position; @@ -317,7 +318,7 @@ impl SearchResult { pub(crate) fn move_down( &mut self, amount: usize, - viewport: &[Row], + viewport: &VecDeque<Row>, grid_height: usize, ) -> bool { let mut found_something = false; @@ -331,7 +332,7 @@ impl SearchResult { // Search the new line for our needle if !self.needle.is_empty() { - if let Some(row) = viewport.first() { + if let Some(row) = viewport.front() { let mut tail = Vec::new(); loop { let tail_idx = 1 + tail.len(); @@ -354,8 +355,8 @@ impl SearchResult { pub(crate) fn move_up( &mut self, amount: usize, - viewport: &[Row], - lines_below: &[Row], + viewport: &VecDeque<Row>, + lines_below: &VecDeque<Row>, grid_height: usize, ) -> bool { let mut found_something = false; @@ -368,7 +369,7 @@ impl SearchResult { // Search the new line for our needle if !self.needle.is_empty() { - if let Some(row) = viewport.last() { + if let Some(row) = viewport.back() { let tail: Vec<&Row> = lines_below.iter().take_while(|r| !r.is_canonical).collect(); let selections = self.search_row(viewport.len() - 1, row, &tail); for selection in selections { diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index 80b413f1..310c3dc4 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -1,7 +1,7 @@ use super::super::Grid; use crate::panes::grid::SixelImageStore; use crate::panes::link_handler::LinkHandler; -use ::insta::assert_snapshot; +use insta::assert_snapshot; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; |
