aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-02 09:15:19 +0100
committerGitHub <noreply@github.com>2026-03-02 09:15:19 +0100
commitee78f7b8f69ec62af10ce9d59e3de27dfcfc4e73 (patch)
tree9fdad1e1fc3a61339eff5f670d435c148ac9a29b
parentfix: translate plugin paths in api (add missing methods) (#4767) (diff)
downloadsl0ppij-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.md1
-rw-r--r--zellij-server/src/panes/grid.rs125
-rw-r--r--zellij-server/src/panes/hyperlink_tracker.rs8
-rw-r--r--zellij-server/src/panes/search.rs11
-rw-r--r--zellij-server/src/panes/unit/grid_tests.rs2
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;