alacritty

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit b1032bcc6b79135f87f327548e43563da05657fb
parent 0b9ae4ce936dfafbf5ea1929a170c97391cdea0b
Author: Christian Duerr <chrisduerr@users.noreply.github.com>
Date:   Wed, 13 Mar 2019 18:55:18 +0000

Add text reflow

Alacritty will now automatically reflow lines and shrink them when they
would usually exceed the new width of the terminal instead of
truncation.

If a line had to be truncated, it will also be reflown into the previous
line after growing the terminal width.

The reflow behavior when not at the bottom of the history is similar to
that of VTE and aims to keep the viewport stationary whenever possible.

Opposed to VTE, reflow will also be performed in the alternate screen
buffer.

There will be bugs when resizing the terminal emulator to a size smaller
than the prompt, though these issues were present in all terminal
emulators with reflow support.

This fixes #591.
Diffstat:
MCHANGELOG.md | 2++
Msrc/cli.rs | 4++--
Msrc/config/mod.rs | 6+++---
Msrc/event.rs | 2+-
Msrc/grid/mod.rs | 134++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/grid/row.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/grid/storage.rs | 207++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/grid/tests.rs | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 4++--
Msrc/message_bar.rs | 2+-
Msrc/term/cell.rs | 36+++++++++++++++++++++++++++---------
Msrc/term/mod.rs | 14+++++++++++---
12 files changed, 608 insertions(+), 70 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to specify starting position with the `--position` flag - New configuration field `window.position` allows specifying the starting position - Added the ability to change the selection color +- Text will reflow instead of truncating when resizing Alacritty ### Fixed @@ -34,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - FreeBSD: SpawnNewInstance will now open new instances in the shell's current working directory as long as linprocfs(5) is mounted on `/compat/linux/proc` - Fix lingering Alacritty window after child process has exited +- Growing the terminal while scrolled up will no longer move the content down ## Version 0.2.9 diff --git a/src/cli.rs b/src/cli.rs @@ -164,8 +164,8 @@ impl Options { } } - options.class = matches.value_of("class").map(|c| c.to_owned()); - options.title = matches.value_of("title").map(|t| t.to_owned()); + options.class = matches.value_of("class").map(ToOwned::to_owned); + options.title = matches.value_of("title").map(ToOwned::to_owned); match matches.occurrences_of("q") { 0 => {}, diff --git a/src/config/mod.rs b/src/config/mod.rs @@ -170,7 +170,7 @@ impl Default for Url { fn deserialize_modifiers<'a, D>(deserializer: D) -> ::std::result::Result<ModifiersState, D::Error> where D: de::Deserializer<'a> { - ModsWrapper::deserialize(deserializer).map(|wrapper| wrapper.into_inner()) + ModsWrapper::deserialize(deserializer).map(ModsWrapper::into_inner) } /// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert @@ -1665,7 +1665,7 @@ impl Config { } None }) - .map(|path| path.into()) + .map(Into::into) } // TODO: Remove old configuration location warning (Deprecated 03/12/2018) @@ -1810,7 +1810,7 @@ impl Config { pub fn path(&self) -> Option<&Path> { self.config_path .as_ref() - .map(|p| p.as_path()) + .map(PathBuf::as_path) } pub fn shell(&self) -> Option<&Shell<'_>> { diff --git a/src/event.rs b/src/event.rs @@ -86,7 +86,7 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> { } fn selection_is_empty(&self) -> bool { - self.terminal.selection().as_ref().map(|s| s.is_empty()).unwrap_or(true) + self.terminal.selection().as_ref().map(Selection::is_empty).unwrap_or(true) } fn clear_selection(&mut self) { diff --git a/src/grid/mod.rs b/src/grid/mod.rs @@ -64,6 +64,12 @@ impl<T: PartialEq> ::std::cmp::PartialEq for Grid<T> { } } +pub trait GridCell { + fn is_empty(&self) -> bool; + fn is_wrap(&self) -> bool; + fn set_wrap(&mut self, wrap: bool); +} + /// Represents the terminal display contents #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Grid<T> { @@ -123,7 +129,7 @@ pub enum ViewportPosition { Below, } -impl<T: Copy + Clone> Grid<T> { +impl<T: GridCell + Copy + Clone> Grid<T> { pub fn new(lines: index::Line, cols: index::Column, scrollback: usize, template: T) -> Grid<T> { let raw = Storage::with_capacity(lines, Row::new(cols, &template)); Grid { @@ -197,6 +203,7 @@ impl<T: Copy + Clone> Grid<T> { &mut self, lines: index::Line, cols: index::Column, + cursor_pos: &mut Point, template: &T, ) { // Check that there's actually work to do and return early if not @@ -211,8 +218,8 @@ impl<T: Copy + Clone> Grid<T> { } match self.cols.cmp(&cols) { - Ordering::Less => self.grow_cols(cols, template), - Ordering::Greater => self.shrink_cols(cols), + Ordering::Less => self.grow_cols(cols, cursor_pos, template), + Ordering::Greater => self.shrink_cols(cols, cursor_pos, template), Ordering::Equal => (), } } @@ -259,20 +266,125 @@ impl<T: Copy + Clone> Grid<T> { } self.scroll_limit = self.scroll_limit.saturating_sub(*lines_added); - } + self.display_offset = self.display_offset.saturating_sub(*lines_added); + } + + fn grow_cols(&mut self, cols: index::Column, cursor_pos: &mut Point, template: &T) { + // Truncate all buffered lines + self.raw.grow_hidden(cols, template); + + let max_lines = self.lines.0 + self.max_scroll_limit; + + // Iterate backwards with indices for mutation during iteration + let mut i = self.raw.len(); + while i > 0 { + i -= 1; + + // Grow the current line if there's wrapped content available + while i >= 1 + && self.raw[i].len() < cols.0 + && self.raw[i].last().map(GridCell::is_wrap) == Some(true) + { + // Remove wrap flag before appending additional cells + if let Some(cell) = self.raw[i].last_mut() { + cell.set_wrap(false); + } + + // Append as many cells from the next line as possible + let len = min(self.raw[i - 1].len(), cols.0 - self.raw[i].len()); + let mut cells = self.raw[i - 1].front_split_off(len); + self.raw[i].append(&mut cells); + + if self.raw[i - 1].is_empty() { + // Remove following line if all cells have been drained + self.raw.remove(i - 1); + + if self.raw.len() < self.lines.0 || self.scroll_limit == 0 { + // Add new line and move lines up if we can't pull from history + self.raw.insert(0, Row::new(cols, template), max_lines); + cursor_pos.line = Line(cursor_pos.line.saturating_sub(1)); + } else { + // Make sure viewport doesn't move if line is outside of the visible area + if i < self.display_offset { + self.display_offset = self.display_offset.saturating_sub(1); + } + + // Remove one line from scrollback, since we just moved it to the viewport + self.scroll_limit = self.scroll_limit.saturating_sub(1); + self.display_offset = min(self.display_offset, self.scroll_limit); + i -= 1; + } + } else if let Some(cell) = self.raw[i].last_mut() { + // Set wrap flag if next line still has cells + cell.set_wrap(true); + } + } - fn grow_cols(&mut self, cols: index::Column, template: &T) { - for row in self.raw.iter_mut_raw() { - row.grow(cols, template); + // Fill remaining cells + if self.raw[i].len() < cols.0 { + self.raw[i].grow(cols, template); + } } - // Update self cols self.cols = cols; } - fn shrink_cols(&mut self, cols: index::Column) { - for row in self.raw.iter_mut_raw() { - row.shrink(cols); + fn shrink_cols(&mut self, cols: index::Column, cursor_pos: &mut Point, template: &T) { + // Truncate all buffered lines + self.raw.shrink_hidden(cols); + + let max_lines = self.lines.0 + self.max_scroll_limit; + + // Iterate backwards with indices for mutation during iteration + let mut i = self.raw.len(); + while i > 0 { + i -= 1; + + if let Some(mut new_row) = self.raw[i].shrink(cols) { + // Set line as wrapped if cells got removed + if let Some(cell) = self.raw[i].last_mut() { + cell.set_wrap(true); + } + + if Some(true) == new_row.last().map(|c| c.is_wrap() && i >= 1) + && new_row.len() < cols.0 + { + // Make sure previous wrap flag doesn't linger around + if let Some(cell) = new_row.last_mut() { + cell.set_wrap(false); + } + + // Add removed cells to start of next row + self.raw[i - 1].append_front(new_row); + } else { + // Make sure viewport doesn't move if line is outside of the visible area + if i < self.display_offset { + self.display_offset = min(self.display_offset + 1, self.max_scroll_limit); + } + + // Make sure new row is at least as long as new width + let occ = new_row.len(); + if occ < cols.0 { + new_row.append(&mut vec![*template; cols.0 - occ]); + } + let row = Row::from_vec(new_row, occ); + + // Add new row with all removed cells + self.raw.insert(i, row, max_lines); + + if cursor_pos.line >= self.lines - 1 { + // Increase scrollback history + self.scroll_limit = min(self.scroll_limit + 1, self.max_scroll_limit); + + // Since inserted might exceed cols, we need to check the same line again + i += 1; + } else { + // Pull content down if cursor is not at the bottom + self.raw.rotate(1); + cursor_pos.line += 1; + } + } + } } self.cols = cols; diff --git a/src/grid/row.rs b/src/grid/row.rs @@ -16,9 +16,10 @@ use std::ops::{Index, IndexMut}; use std::ops::{Range, RangeTo, RangeFrom, RangeFull, RangeToInclusive}; -use std::cmp::{max, min}; +use std::cmp::{min, max}; use std::slice; +use crate::grid::GridCell; use crate::index::Column; /// A row in the grid @@ -43,7 +44,7 @@ impl<T: PartialEq> PartialEq for Row<T> { } } -impl<T: Copy + Clone> Row<T> { +impl<T: Copy> Row<T> { pub fn new(columns: Column, template: &T) -> Row<T> { Row { inner: vec![*template; *columns], @@ -52,52 +53,102 @@ impl<T: Copy + Clone> Row<T> { } pub fn grow(&mut self, cols: Column, template: &T) { - assert!(self.len() < * cols); + if self.inner.len() >= cols.0 { + return; + } + + self.inner.append(&mut vec![*template; cols.0 - self.len()]); + } + + pub fn shrink(&mut self, cols: Column) -> Option<Vec<T>> + where + T: GridCell + { + if self.inner.len() <= cols.0 { + return None; + } + + // Split off cells for a new row + let mut new_row = self.inner.split_off(cols.0); + let index = new_row.iter().rposition(|c| !c.is_empty()).map(|i| i + 1).unwrap_or(0); + new_row.truncate(index); + + self.occ = min(self.occ, *cols); - while self.len() != *cols { - self.inner.push(*template); + if new_row.is_empty() { + None + } else { + Some(new_row) } } /// Resets contents to the contents of `other` #[inline(never)] pub fn reset(&mut self, other: &T) { - let occ = self.occ; - for item in &mut self.inner[..occ] { + for item in &mut self.inner[..self.occ] { *item = *other; } - self.occ = 0; } } #[allow(clippy::len_without_is_empty)] impl<T> Row<T> { - pub fn shrink(&mut self, cols: Column) { - while self.len() != *cols { - self.inner.pop(); + #[inline] + pub fn from_vec(vec: Vec<T>, occ: usize) -> Row<T> { + Row { + inner: vec, + occ, } - - self.occ = min(self.occ, *cols); } + #[inline] pub fn len(&self) -> usize { self.inner.len() } - pub fn iter(&self) -> slice::Iter<'_, T> { - self.inner.iter() + #[inline] + pub fn last(&self) -> Option<&T> { + self.inner.last() } -} + #[inline] + pub fn last_mut(&mut self) -> Option<&mut T> { + self.occ = self.inner.len(); + self.inner.last_mut() + } -impl<'a, T> IntoIterator for &'a Row<T> { - type Item = &'a T; - type IntoIter = slice::Iter<'a, T>; + #[inline] + pub fn append(&mut self, vec: &mut Vec<T>) + where + T: GridCell + { + self.inner.append(vec); + self.occ = self.inner.iter().rposition(|c| !c.is_empty()).map(|i| i + 1).unwrap_or(0); + } #[inline] - fn into_iter(self) -> slice::Iter<'a, T> { - self.iter() + pub fn append_front(&mut self, mut vec: Vec<T>) { + self.occ += vec.len(); + vec.append(&mut self.inner); + self.inner = vec; + } + + #[inline] + pub fn is_empty(&self) -> bool + where + T: GridCell + { + self.inner.iter().all(|c| c.is_empty()) + } + + #[inline] + pub fn front_split_off(&mut self, at: usize) -> Vec<T> { + self.occ = self.occ.saturating_sub(at); + + let mut split = self.inner.split_off(at); + std::mem::swap(&mut split, &mut self.inner); + split } } diff --git a/src/grid/storage.rs b/src/grid/storage.rs @@ -12,11 +12,11 @@ /// implementation is provided. Anything from Vec that should be exposed must be /// done so manually. use std::ops::{Index, IndexMut}; -use std::slice; use static_assertions::assert_eq_size; -use crate::index::Line; +use crate::index::{Column, Line}; +use crate::grid::GridCell; use super::Row; /// Maximum number of invisible lines before buffer is resized @@ -196,6 +196,7 @@ impl<T> Storage<T> { self.len } + #[inline] /// Compute actual index in underlying storage given the requested index. fn compute_index(&self, requested: usize) -> usize { debug_assert!(requested < self.len); @@ -250,18 +251,7 @@ impl<T> Storage<T> { } } - /// Iterate over *all* entries in the underlying buffer - /// - /// This includes hidden entries. - /// - /// XXX This suggests that Storage is a leaky abstraction. Ultimately, this - /// is needed because of the grow lines functionality implemented on - /// this type, and maybe that's where the leak is necessitating this - /// accessor. - pub fn iter_mut_raw<'a>(&'a mut self) -> slice::IterMut<'a, Row<T>> { - self.inner.iter_mut() - } - + #[inline] pub fn rotate(&mut self, count: isize) { debug_assert!(count.abs() as usize <= self.inner.len()); @@ -270,9 +260,75 @@ impl<T> Storage<T> { } // Fast path + #[inline] pub fn rotate_up(&mut self, count: usize) { self.zero = (self.zero + count) % self.inner.len(); } + + #[inline] + pub fn insert(&mut self, index: usize, row: Row<T>, max_lines: usize) { + let index = self.compute_index(index); + self.inner.insert(index, row); + + if index < self.zero { + self.zero += 1; + } + + if self.len < max_lines { + self.len += 1; + } + } + + #[inline] + pub fn remove(&mut self, index: usize) -> Row<T> { + let index = self.compute_index(index); + if index < self.zero { + self.zero -= 1; + } + self.len -= 1; + + self.inner.remove(index) + } + + /// Shrink columns of hidden buffered lines. + /// + /// XXX This suggests that Storage is a leaky abstraction. Ultimately, this + /// is needed because of the grow/shrink lines functionality. + #[inline] + pub fn shrink_hidden(&mut self, cols: Column) + where + T: GridCell + Copy + { + let start = self.zero + self.len; + let end = self.zero + self.inner.len(); + for mut i in start..end { + if i >= self.inner.len() { + i -= self.inner.len(); + } + + self.inner[i].shrink(cols); + } + } + + /// Grow columns of hidden buffered lines. + /// + /// XXX This suggests that Storage is a leaky abstraction. Ultimately, this + /// is needed because of the grow/shrink lines functionality. + #[inline] + pub fn grow_hidden(&mut self, cols: Column, template: &T) + where + T: Copy + Clone + { + let start = self.zero + self.len; + let end = self.zero + self.inner.len(); + for mut i in start..end { + if i >= self.inner.len() { + i -= self.inner.len(); + } + + self.inner[i].grow(cols, template); + } + } } impl<T> Index<usize> for Storage<T> { @@ -308,9 +364,6 @@ impl<T> IndexMut<Line> for Storage<T> { } } -#[cfg(test)] -use crate::index::Column; - /// Grow the buffer one line at the end of the buffer /// /// Before: @@ -693,3 +746,123 @@ fn initialize() { assert_eq!(storage.zero, shrinking_expected.zero); assert_eq!(storage.len, shrinking_expected.len); } + +#[test] +fn insert() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.insert(2, Row::new(Column(1), &'-'), 100); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 7, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} + +#[test] +fn insert_truncate_max() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.insert(2, Row::new(Column(1), &'-'), 6); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} + +#[test] +fn insert_at_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.insert(0, Row::new(Column(1), &'-'), 6); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} diff --git a/src/grid/tests.rs b/src/grid/tests.rs @@ -16,6 +16,20 @@ use super::{Grid, BidirectionalIterator}; use crate::index::{Point, Line, Column}; +use crate::term::cell::{Cell, Flags}; +use crate::grid::GridCell; + +impl GridCell for usize { + fn is_empty(&self) -> bool { + false + } + + fn is_wrap(&self) -> bool { + false + } + + fn set_wrap(&mut self, _wrap: bool) {} +} // Scroll up moves lines upwards #[test] @@ -123,3 +137,163 @@ fn test_iter() { assert_eq!(None, final_iter.next()); assert_eq!(Some(&23), final_iter.prev()); } + +#[test] +fn shrink_reflow() { + let mut grid = Grid::new(Line(1), Column(5), 2, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = cell('2'); + grid[Line(0)][Column(2)] = cell('3'); + grid[Line(0)][Column(3)] = cell('4'); + grid[Line(0)][Column(4)] = cell('5'); + + grid.resize(Line(1), Column(2), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 3); + + assert_eq!(grid[2].len(), 2); + assert_eq!(grid[2][Column(0)], cell('1')); + assert_eq!(grid[2][Column(1)], wrap_cell('2')); + + assert_eq!(grid[1].len(), 2); + assert_eq!(grid[1][Column(0)], cell('3')); + assert_eq!(grid[1][Column(1)], wrap_cell('4')); + + assert_eq!(grid[0].len(), 2); + assert_eq!(grid[0][Column(0)], cell('5')); + assert_eq!(grid[0][Column(1)], Cell::default()); +} + +#[test] +fn shrink_reflow_twice() { + let mut grid = Grid::new(Line(1), Column(5), 2, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = cell('2'); + grid[Line(0)][Column(2)] = cell('3'); + grid[Line(0)][Column(3)] = cell('4'); + grid[Line(0)][Column(4)] = cell('5'); + + grid.resize(Line(1), Column(4), &mut Point::new(Line(0), Column(0)), &Cell::default()); + grid.resize(Line(1), Column(2), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 3); + + assert_eq!(grid[2].len(), 2); + assert_eq!(grid[2][Column(0)], cell('1')); + assert_eq!(grid[2][Column(1)], wrap_cell('2')); + + assert_eq!(grid[1].len(), 2); + assert_eq!(grid[1][Column(0)], cell('3')); + assert_eq!(grid[1][Column(1)], wrap_cell('4')); + + assert_eq!(grid[0].len(), 2); + assert_eq!(grid[0][Column(0)], cell('5')); + assert_eq!(grid[0][Column(1)], Cell::default()); +} + +#[test] +fn shrink_reflow_empty_cell_inside_line() { + let mut grid = Grid::new(Line(1), Column(5), 3, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = Cell::default(); + grid[Line(0)][Column(2)] = cell('3'); + grid[Line(0)][Column(3)] = cell('4'); + grid[Line(0)][Column(4)] = Cell::default(); + + grid.resize(Line(1), Column(2), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 2); + + assert_eq!(grid[1].len(), 2); + assert_eq!(grid[1][Column(0)], cell('1')); + assert_eq!(grid[1][Column(1)], wrap_cell(' ')); + + assert_eq!(grid[0].len(), 2); + assert_eq!(grid[0][Column(0)], cell('3')); + assert_eq!(grid[0][Column(1)], cell('4')); + + grid.resize(Line(1), Column(1), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 4); + + assert_eq!(grid[3].len(), 1); + assert_eq!(grid[3][Column(0)], wrap_cell('1')); + + assert_eq!(grid[2].len(), 1); + assert_eq!(grid[2][Column(0)], wrap_cell(' ')); + + assert_eq!(grid[1].len(), 1); + assert_eq!(grid[1][Column(0)], wrap_cell('3')); + + assert_eq!(grid[0].len(), 1); + assert_eq!(grid[0][Column(0)], cell('4')); +} + +#[test] +fn grow_reflow() { + let mut grid = Grid::new(Line(2), Column(2), 0, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = wrap_cell('2'); + grid[Line(1)][Column(0)] = cell('3'); + grid[Line(1)][Column(1)] = Cell::default(); + + grid.resize(Line(2), Column(3), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 2); + + assert_eq!(grid[1].len(), 3); + assert_eq!(grid[1][Column(0)], cell('1')); + assert_eq!(grid[1][Column(1)], cell('2')); + assert_eq!(grid[1][Column(2)], cell('3')); + + // Make sure rest of grid is empty + assert_eq!(grid[0].len(), 3); + assert_eq!(grid[0][Column(0)], Cell::default()); + assert_eq!(grid[0][Column(1)], Cell::default()); + assert_eq!(grid[0][Column(2)], Cell::default()); +} + +#[test] +fn grow_reflow_multiline() { + let mut grid = Grid::new(Line(3), Column(2), 0, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = wrap_cell('2'); + grid[Line(1)][Column(0)] = cell('3'); + grid[Line(1)][Column(1)] = wrap_cell('4'); + grid[Line(2)][Column(0)] = cell('5'); + grid[Line(2)][Column(1)] = cell('6'); + + grid.resize(Line(3), Column(6), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 3); + + assert_eq!(grid[2].len(), 6); + assert_eq!(grid[2][Column(0)], cell('1')); + assert_eq!(grid[2][Column(1)], cell('2')); + assert_eq!(grid[2][Column(2)], cell('3')); + assert_eq!(grid[2][Column(3)], cell('4')); + assert_eq!(grid[2][Column(4)], cell('5')); + assert_eq!(grid[2][Column(5)], cell('6')); + + // Make sure rest of grid is empty + // https://github.com/rust-lang/rust-clippy/issues/3788 + #[allow(clippy::needless_range_loop)] + for r in 0..2 { + assert_eq!(grid[r].len(), 6); + for c in 0..6 { + assert_eq!(grid[r][Column(c)], Cell::default()); + } + } +} + +fn cell(c: char) -> Cell { + let mut cell = Cell::default(); + cell.c = c; + cell +} + +fn wrap_cell(c: char) -> Cell { + let mut cell = cell(c); + cell.flags.insert(Flags::WRAPLINE); + cell +} diff --git a/src/main.rs b/src/main.rs @@ -45,7 +45,7 @@ use std::os::unix::io::AsRawFd; #[cfg(target_os = "macos")] use alacritty::locale; use alacritty::{cli, event, die}; -use alacritty::config::{self, Config}; +use alacritty::config::{self, Config, Monitor}; use alacritty::display::Display; use alacritty::event_loop::{self, EventLoop, Msg}; use alacritty::logging; @@ -221,7 +221,7 @@ fn run( let mut terminal_lock = processor.process_events(&terminal, display.window()); // Handle config reloads - if let Some(ref path) = config_monitor.as_ref().and_then(|monitor| monitor.pending()) { + if let Some(ref path) = config_monitor.as_ref().and_then(Monitor::pending) { // Clear old config messages from bar terminal_lock.message_buffer_mut().remove_topic(config::SOURCE_FILE_PATH); diff --git a/src/message_bar.rs b/src/message_bar.rs @@ -190,7 +190,7 @@ impl MessageBuffer { .messages .try_iter() .take(self.messages.len()) - .filter(|m| m.topic().map(|s| s.as_str()) != Some(topic)) + .filter(|m| m.topic().map(String::as_str) != Some(topic)) { let _ = self.tx.send(msg); } diff --git a/src/term/cell.rs b/src/term/cell.rs @@ -14,7 +14,7 @@ use bitflags::bitflags; use crate::ansi::{NamedColor, Color}; -use crate::grid; +use crate::grid::{self, GridCell}; use crate::index::Column; // Maximum number of zerowidth characters which will be stored per cell. @@ -62,6 +62,32 @@ impl Default for Cell { } +impl GridCell for Cell { + #[inline] + fn is_empty(&self) -> bool { + (self.c == ' ' || self.c == '\t') + && self.extra[0] == ' ' + && self.bg == Color::Named(NamedColor::Background) + && !self + .flags + .intersects(Flags::INVERSE | Flags::UNDERLINE | Flags::STRIKEOUT | Flags::WRAPLINE) + } + + #[inline] + fn is_wrap(&self) -> bool { + self.flags.contains(Flags::WRAPLINE) + } + + #[inline] + fn set_wrap(&mut self, wrap: bool) { + if wrap { + self.flags.insert(Flags::WRAPLINE); + } else { + self.flags.remove(Flags::WRAPLINE); + } + } +} + /// Get the length of occupied cells in a line pub trait LineLength { /// Calculate the occupied line length @@ -114,14 +140,6 @@ impl Cell { } #[inline] - pub fn is_empty(&self) -> bool { - (self.c == ' ' || self.c == '\t') - && self.extra[0] == ' ' - && self.bg == Color::Named(NamedColor::Background) - && !self.flags.intersects(Flags::INVERSE | Flags::UNDERLINE | Flags::STRIKEOUT) - } - - #[inline] pub fn reset(&mut self, template: &Cell) { // memcpy template to self *self = *template; diff --git a/src/term/mod.rs b/src/term/mod.rs @@ -23,7 +23,10 @@ use unicode_width::UnicodeWidthChar; use font::{self, Size}; use crate::ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle}; -use crate::grid::{BidirectionalIterator, Grid, Indexed, IndexRegion, DisplayIter, Scroll, ViewportPosition}; +use crate::grid::{ + BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, + ViewportPosition, +}; use crate::index::{self, Point, Column, Line, IndexRange, Contains, RangeInclusive, Linear}; use crate::selection::{self, Selection, Locations}; use crate::config::{Config, VisualBellAnimation}; @@ -1246,8 +1249,13 @@ impl Term { debug!("New num_cols is {} and num_lines is {}", num_cols, num_lines); // Resize grids to new size - self.grid.resize(num_lines, num_cols, &Cell::default()); - self.alt_grid.resize(num_lines, num_cols, &Cell::default()); + let alt_cursor_point = if self.mode.contains(TermMode::ALT_SCREEN) { + &mut self.cursor_save.point + } else { + &mut self.cursor_save_alt.point + }; + self.grid.resize(num_lines, num_cols, &mut self.cursor.point, &Cell::default()); + self.alt_grid.resize(num_lines, num_cols, alt_cursor_point, &Cell::default()); // Reset scrolling region to new size self.scroll_region = Line(0)..self.grid.num_lines();