1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 21:32:13 +03:00

re-wrap lines when resizing

Adds logic to resize handling that will consider the original logical
line length when the width of the terminal is changed.

The intent is that this will cause the text to be re-flowed as if it had
been printed into the terminal at the new width.  Lines that were
wrapped due to hittin the margin will be un-wrapped and made into a
single logical line, and then split into chunks of the new width.

This can cause new lines to be generated in the scrollback when
making the terminal narrower.  To avoid losing the top of the buffer
in that case, the rewrapping logic will prune blank lines off the
bottom.

This is a pretty simplistic brute force algorithm: each of the lines
will be visited and split, and for large scrollback buffers this could
be relatively costly with a busy live resize.  We don't have much choice
in the current implementation.

refs: https://github.com/wez/wezterm/issues/14
This commit is contained in:
Wez Furlong 2020-01-10 23:58:12 -08:00
parent 44b3c412b6
commit e6b4aa835a
4 changed files with 198 additions and 4 deletions

View File

@ -76,11 +76,73 @@ impl Screen {
scrollback_size(&self.config, self.allow_scrollback)
}
fn rewrap_lines(&mut self, physical_cols: usize) {
let mut rewrapped = VecDeque::new();
let mut logical_line: Option<Line> = None;
for mut line in self.lines.drain(..) {
line.invalidate_implicit_hyperlinks();
if line.last_cell_was_wrapped() {
line.set_last_cell_was_wrapped(false);
logical_line = Some(match logical_line.take() {
None => line,
Some(mut prior) => {
prior.append_line(line);
prior
}
});
continue;
}
let line = match logical_line.take() {
None => line,
Some(mut prior) => {
prior.append_line(line);
prior
}
};
if line.cells().len() <= physical_cols {
rewrapped.push_back(line);
} else {
for line in line.wrap(physical_cols) {
rewrapped.push_back(line);
}
}
}
self.lines = rewrapped;
// If we resized narrower and generated additional lines,
// we may need to scroll the lines to make room. However,
// if the bottom line(s) are whitespace, we'll prune those
// out first in the rewrap case so that we don't lose any
// real information off the top of the scrollback
while self.lines.len() > self.physical_rows
&& self.lines.back().map(Line::is_whitespace).unwrap_or(false)
{
self.lines.pop_back();
}
// If we resized wider and the rewrap resulted in fewer
// lines than the viewport size, pad us back out to the
// viewport size
while self.lines.len() < self.physical_rows {
self.lines.push_back(Line::with_width(physical_cols));
}
}
/// Resize the physical, viewable portion of the screen
pub fn resize(&mut self, physical_rows: usize, physical_cols: usize) {
let physical_rows = physical_rows.max(1);
let physical_cols = physical_cols.max(1);
if physical_cols != self.physical_cols {
// Check to see if we need to rewrap lines that were
// wrapped due to reaching the right hand side of the terminal.
// For each one that we find, we need to join it with its
// successor and then re-split it
self.rewrap_lines(physical_cols);
}
let capacity = physical_rows + self.scrollback_size();
let current_capacity = self.lines.capacity();
if capacity > current_capacity {
@ -211,11 +273,11 @@ impl Screen {
}
};
let last = match self.stable_row_to_phys(range.end - 1) {
let last = match self.stable_row_to_phys(range.end.saturating_sub(1)) {
Some(last) => last,
None => {
let last = self.lines.len() - 1;
return last - range_len..last + 1;
return last.saturating_sub(range_len)..last + 1;
}
};

View File

@ -1296,12 +1296,16 @@ impl TerminalState {
let position = position.max(0);
let rows = self.screen().physical_rows;
let avail_scrollback = self.screen().lines.len() - rows;
let avail_scrollback = self.screen().lines.len().saturating_sub(rows);
let position = position.min(avail_scrollback as i64);
self.viewport_offset = position;
let top = self.screen().lines.len() - (rows + position as usize);
let top = self
.screen()
.lines
.len()
.saturating_sub(rows + position as usize);
{
let screen = self.screen_mut();
for y in top..top + rows {

View File

@ -524,6 +524,73 @@ fn test_delete_lines() {
term.assert_dirty_lines(&[1, 2, 3, 4], None);
}
/// Test the behavior of wrapped lines when we resize the terminal
/// wider and then narrower.
#[test]
fn test_resize_wrap() {
const LINES: usize = 8;
let mut term = TestTerm::new(LINES, 4, 0);
term.print("111\r\n2222aa\r\n333\r\n");
assert_visible_contents(
&term,
&[
"111 ", "2222", "aa ", "333 ", " ", " ", " ", " ",
],
);
term.resize(LINES, 5, 0, 0);
assert_visible_contents(
&term,
&["111 ", "2222a", "a", "333 ", " ", " ", " ", " "],
);
term.resize(LINES, 6, 0, 0);
assert_visible_contents(
&term,
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
);
term.resize(LINES, 7, 0, 0);
assert_visible_contents(
&term,
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
);
term.resize(LINES, 8, 0, 0);
assert_visible_contents(
&term,
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
);
// Resize smaller again
term.resize(LINES, 7, 0, 0);
assert_visible_contents(
&term,
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
);
term.resize(LINES, 6, 0, 0);
assert_visible_contents(
&term,
&[
"111 ", "2222aa", "333 ", " ", " ", " ", " ", " ",
],
);
term.resize(LINES, 5, 0, 0);
assert_visible_contents(
&term,
&["111 ", "2222a", "a", "333 ", " ", " ", " ", " "],
);
term.resize(LINES, 4, 0, 0);
assert_visible_contents(
&term,
&["111 ", "2222", "aa", "333 ", " ", " ", " ", " "],
);
}
#[test]
fn test_scrollup() {
let mut term = TestTerm::new(2, 1, 4);

View File

@ -81,6 +81,37 @@ impl Line {
self.bits |= LineBits::DIRTY;
}
/// Wrap the line so that it fits within the provided width.
/// Returns the list of resultant line(s)
pub fn wrap(mut self, width: usize) -> Vec<Self> {
if let Some(end_idx) = self.cells.iter().rposition(|c| c.str() != " ") {
self.cells.resize(end_idx + 1, Cell::default());
let mut lines: Vec<_> = self
.cells
.chunks_mut(width)
.map(|chunk| {
let mut line = Line {
cells: chunk.to_vec(),
bits: LineBits::DIRTY,
};
if line.cells.len() == width {
// Ensure that we don't forget that we wrapped
line.set_last_cell_was_wrapped(true);
}
line
})
.collect();
// The last of the chunks wasn't actually wrapped
lines
.last_mut()
.map(|line| line.set_last_cell_was_wrapped(false));
lines
} else {
vec![self]
}
}
/// Check whether the dirty bit is set.
/// If it is set, then something about the line has changed since
/// the dirty bit was last cleared.
@ -348,6 +379,36 @@ impl Line {
&self.cells
}
/// Return true if the line consists solely of whitespace cells
pub fn is_whitespace(&self) -> bool {
self.cells.iter().all(|c| c.str() == " ")
}
/// Return true if the last cell in the line has the wrapped attribute,
/// indicating that the following line is logically a part of this one.
pub fn last_cell_was_wrapped(&self) -> bool {
self.cells
.last()
.map(|c| c.attrs().wrapped())
.unwrap_or(false)
}
/// Adjust the value of the wrapped attribute on the last cell of this
/// line.
pub fn set_last_cell_was_wrapped(&mut self, wrapped: bool) {
if let Some(cell) = self.cells.last_mut() {
cell.attrs_mut().set_wrapped(wrapped);
}
}
/// Concatenate the cells from other with this line, appending them
/// to this line.
/// This function is used by rewrapping logic when joining wrapped
/// lines back together.
pub fn append_line(&mut self, mut other: Line) {
self.cells.append(&mut other.cells);
}
/// mutable access the cell data, but the caller must take care
/// to only mutate attributes rather than the cell textual content.
/// Use set_cell if you need to modify the textual content of the