Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
use std::ops::{Index, IndexMut};
use crate::terminal::model::index::IndexRange;
/// Default tab interval, corresponding to terminfo `it` value.
const INITIAL_TABSTOPS: usize = 8;
#[derive(Clone)]
pub struct TabStops {
tabs: Vec<bool>,
}
impl TabStops {
#[inline]
pub fn new(num_cols: usize) -> TabStops {
TabStops {
tabs: IndexRange::from(0..num_cols)
.map(|i| i % INITIAL_TABSTOPS == 0)
.collect::<Vec<bool>>(),
}
}
/// Remove all tabstops.
#[inline]
pub fn clear_all(&mut self) {
unsafe {
std::ptr::write_bytes(self.tabs.as_mut_ptr(), 0, self.tabs.len());
}
}
/// Increase tabstop capacity.
#[inline]
pub fn resize(&mut self, num_cols: usize) {
let mut index = self.tabs.len();
self.tabs.resize_with(num_cols, || {
let is_tabstop = index.is_multiple_of(INITIAL_TABSTOPS);
index += 1;
is_tabstop
});
}
}
impl Index<usize> for TabStops {
type Output = bool;
fn index(&self, index: usize) -> &bool {
&self.tabs[index]
}
}
impl IndexMut<usize> for TabStops {
fn index_mut(&mut self, index: usize) -> &mut bool {
self.tabs.index_mut(index)
}
}
@@ -0,0 +1,573 @@
use std::cmp::Ordering;
use std::ops::{Range, RangeInclusive};
use bimap::BiMap;
use itertools::Itertools;
use crate::terminal::model::index::Point;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DisplaySource {
CursorLine,
FilterMatch,
FilterContext,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DisplayedRows {
pub range: RangeInclusive<usize>,
pub source: DisplaySource,
}
impl DisplayedRows {
#[cfg(test)]
pub fn new(range: RangeInclusive<usize>, source: DisplaySource) -> Self {
Self { range, source }
}
}
/// Whether or not to respect the displayed output/filter when
/// retrieving the grid contents.
#[derive(Copy, Clone)]
pub enum RespectDisplayedOutput {
/// Points are assumed to represent the displayed location in the grid.
Yes,
/// Points are assumed to represent the original location in the grid.
No,
}
/// Structure to represent a subset of rows from the grid that we want to
/// make visible when rendering.
#[derive(Clone, Default, Debug)]
pub struct DisplayedOutput {
/// The rows that we want to display in the grid, represented by their
/// row indices in the grid. The ranges must be non-overlapping and in
/// sorted order, from lowest to highest.
displayed_rows: Vec<DisplayedRows>,
/// Height of the displayed rows in the grid. This is equivalent to the
/// number of displayed rows.
height: usize,
row_translation_map: RowTranslationMap,
}
/// A mapping to translate locations in the grid between the original row and the offset row after filtering.
/// NOTE: `Left` = original row and `Right` = offset/translated row
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RowTranslationMap {
pub(crate) inner: BiMap<usize, usize>,
}
impl From<BiMap<usize, usize>> for RowTranslationMap {
fn from(value: BiMap<usize, usize>) -> Self {
Self { inner: value }
}
}
impl RowTranslationMap {
/// Translates a point from its original location to the displayed point's location (the offset with the filter applied).
///
/// NOTE: If the original location is not found in the row translation map,
/// then it will return the given point.
pub fn maybe_translate_point_from_original_to_displayed(&self, original_point: Point) -> Point {
self.inner
.get_by_left(&original_point.row)
.map(|translated_row| Point {
row: *translated_row,
col: original_point.col,
})
.unwrap_or_else(|| {
log::warn!("Could not translate point {original_point:?} to its displayed location, returning given point instead");
original_point
})
}
/// Translates a point from its displayed location (the offset with the filter applied) to its original location.
///
/// NOTE: If the displayed location is not found in the row translation map,
/// then it will return the given point.
pub fn maybe_translate_point_from_displayed_to_original(
&self,
displayed_point: Point,
) -> Point {
self.inner
.get_by_right(&displayed_point.row)
.map(|original_row| Point {
row: *original_row,
col: displayed_point.col,
})
.unwrap_or_else(|| {
log::warn!("Could not translate point {displayed_point:?} to its original location, returning given point instead");
displayed_point
})
}
/// Translates a row from its displayed location (the offset with the filter applied) to its original location.
///
/// NOTE: If the displayed location is not found in the row translation map,
/// then it will return the given row.
pub fn maybe_translate_row_from_displayed_to_original(&self, displayed_row: usize) -> usize {
*self.inner
.get_by_right(&displayed_row)
.unwrap_or_else(|| {
log::warn!("Could not translate row {displayed_row:?} to its original location, returning given row instead");
&displayed_row
})
}
/// Returns true iff the `row` is a displayed row.
pub fn is_displayed_row(&self, row: usize) -> bool {
self.inner.contains_left(&row)
}
}
impl DisplayedOutput {
/// Constructs a new instance of DisplayedOutput from the given displayed
/// rows.
///
/// This is faster than creating a new instance via DisplayedOutput::default()
/// and calling extend_displayed_lines.
///
/// Caller must ensure that the row ranges in displayed_rows are:
/// - In ascending order
/// - Representing one or more whole logical lines. The row ranges cannot
/// only have part of a logical line.
pub fn new_from_displayed_lines(displayed_lines: Vec<DisplayedRows>) -> Self {
// Assert that the displayed rows are in ascending order.
debug_assert!(displayed_lines
.windows(2)
.all(|w| w[0].range.end() < w[1].range.start()));
let mut height = 0;
for rows in displayed_lines.iter() {
height += (rows.range.end() - rows.range.start()) + 1;
}
let mut original_to_offset_rows = BiMap::with_capacity(height);
for (offset, row) in displayed_lines
.iter()
.map(|rows| &rows.range)
.cloned()
.flatten()
.enumerate()
{
original_to_offset_rows.insert(row, offset);
}
Self {
displayed_rows: displayed_lines,
height,
row_translation_map: RowTranslationMap {
inner: original_to_offset_rows,
},
}
}
/// Get the height of the displayed rows.
pub fn height(&self) -> usize {
self.height
}
pub fn displayed_rows(&self) -> &[DisplayedRows] {
&self.displayed_rows
}
/// Returns an iterator over the indices in the grid of the displayed rows.
pub fn rows(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
self.displayed_rows
.iter()
.map(|rows| &rows.range)
.cloned()
.flatten()
}
/// Marks a set of row ranges representing logical lines in the grid as
/// visible at the end of the currently displayed lines.
///
/// Caller must ensure that the given lines are in ascending order and
/// greater than all existing lines.
pub fn extend_displayed_lines(&mut self, displayed_lines: Vec<DisplayedRows>) {
for rows in displayed_lines.iter() {
self.height += (rows.range.end() - rows.range.start()) + 1;
}
self.append_to_row_translation(displayed_lines.iter());
self.displayed_rows.extend(displayed_lines);
// Assert that displayed_rows is still in ascending order.
debug_assert!(self
.displayed_rows
.windows(2)
.all(|w| w[0].range.end() < w[1].range.start()));
}
/// Marks a set of row ranges representing logical lines in the grid as
/// visible at the beginning of the currently displayed lines.
///
/// Caller must ensure that the given lines are in ascending order and
/// less than all existing lines.
pub fn prepend_displayed_lines(&mut self, displayed_lines: Vec<DisplayedRows>) {
for rows in displayed_lines.iter() {
self.height += (rows.range.end() - rows.range.start()) + 1;
}
self.displayed_rows.splice(0..0, displayed_lines);
self.prepend_to_row_translation();
// Assert that displayed_rows is still in ascending order.
debug_assert!(self
.displayed_rows
.windows(2)
.all(|w| w[0].range.end() < w[1].range.start()));
}
/// Replaces the specified range in the existing displayed lines with new
/// displayed lines. The new displayed lines need not be the same length
/// as the specified range.
///
/// Caller must ensure that the specified range is valid and the new lines
/// are inserted in ascending order.
pub fn splice_displayed_lines(
&mut self,
replace_range: Range<usize>,
displayed_lines: Vec<DisplayedRows>,
) -> Vec<DisplayedRows> {
for rows in displayed_lines.iter() {
self.height += (rows.range.end() - rows.range.start()) + 1;
}
let removed = self
.displayed_rows
.splice(replace_range.clone(), displayed_lines)
.collect_vec();
self.replace_rows_from_row_translation(replace_range);
for rows in removed.iter() {
self.height = self
.height
.saturating_sub((rows.range.end() - rows.range.start()) + 1);
}
// Assert that displayed_rows is still in ascending order.
debug_assert!(self
.displayed_rows
.windows(2)
.all(|w| w[0].range.end() < w[1].range.start()));
removed
}
/// Truncates the displayed rows so only rows up to and including the provided
/// row are kept.
///
/// e.g. If we are displaying rows [2..=4, 6..=10, 16..=17], truncating to
/// to row 8 would yield [2..=4, 6..=8].
///
/// Mainly used when finishing a block, as the grid gets truncated to the
/// current cursor position.
pub fn truncate_to_row(&mut self, row: usize) {
if let Some(first) = self.displayed_rows.first() {
// If we are truncating before the first rows, just clear it all.
if row < *first.range.start() {
self.displayed_rows.clear();
self.height = 0;
return;
}
} else if let Some(last) = self.displayed_rows.last() {
// If we are truncating after the last rows, do nothing.
if *last.range.end() < row {
return;
}
}
// Find the index of the range that we want to truncate from. Searches
// from right-to-left assuming that most of the time, the row to
// truncate to will be near the end of displayed_rows.
let mut truncate_idx = None;
for (i, rows) in self.displayed_rows.iter().enumerate().rev() {
if row > *rows.range.end() {
break;
}
truncate_idx = Some(i);
}
let Some(truncate_idx) = truncate_idx else {
return;
};
let mut truncated = self.displayed_rows.drain(truncate_idx..);
let mut partial_rows = None;
// We need to handle the first truncated range specially. We may be
// truncating in the middle of the range, in which case we want to
// keep the partial row range before the truncate point.
if let Some(first_truncated) = truncated.next() {
self.height = self
.height
.saturating_sub((first_truncated.range.end() - first_truncated.range.start()) + 1);
if first_truncated.range.contains(&row) {
partial_rows = Some(DisplayedRows {
range: *first_truncated.range.start()..=row,
source: first_truncated.source,
});
}
}
// Subtract the remaining truncated rows from the height.
for rows in truncated {
self.height = self
.height
.saturating_sub((rows.range.end() - rows.range.start()) + 1);
}
if let Some(partial_rows) = partial_rows {
self.height += (partial_rows.range.end() - partial_rows.range.start()) + 1;
self.displayed_rows.push(partial_rows);
}
}
/// Updates the row translation map assuming that we only appended displayed rows.
/// NOTE: Must be called before updating `self.displayed_rows`.
fn append_to_row_translation<'a, I>(&mut self, new_line_ranges: I)
where
I: IntoIterator<Item = &'a DisplayedRows>,
{
let last_displayed_row = self
.displayed_rows
.last()
.map(|last_displayed_row| *last_displayed_row.range.end())
.unwrap_or(0);
let mut offset_row = if let Some(last_row_offset) = self
.row_translation_map
.inner
.get_by_left(&last_displayed_row)
{
last_row_offset + 1
} else {
0
};
for rows in new_line_ranges {
for row in rows.range.clone() {
self.row_translation_map.inner.insert(row, offset_row);
offset_row += 1;
}
}
}
/// Updates the row translation map assuming that we only prepended displayed rows.
/// NOTE: must be called after updating `self.displayed_rows`.
fn prepend_to_row_translation(&mut self) {
// None of the current entries are valid.
self.row_translation_map.inner.clear();
for (offset, row) in self
.displayed_rows
.iter()
.map(|row_range| &row_range.range)
.cloned()
.flatten()
.enumerate()
{
self.row_translation_map.inner.insert(row, offset);
}
}
/// Updates the row translation map assuming that we replaced some of the displayed rows.
/// `replace_range` represents the range of indices in displayed rows that has been updated.
/// NOTE: Must be called after updating `self.displayed_rows`.
fn replace_rows_from_row_translation(&mut self, replace_range: Range<usize>) {
let first_row_before_replace_range = if replace_range.start == 0 {
// We are replacing the beginning row(s).
None
} else {
self.displayed_rows
.get(replace_range.start - 1)
.map(|first_replaced| first_replaced.range.end())
};
let mut offset_row =
first_row_before_replace_range.map_or(0, |first_row_before_replace_range| {
if let Some(row) = self
.row_translation_map
.inner
.get_by_left(first_row_before_replace_range)
{
row + 1
} else {
// There is no offset which means we are at the beginning.
0
}
});
// Clear all rows from our start offset row to the last offset row.
let last_offset_row = self.row_translation_map.inner.len();
for row in offset_row..=last_offset_row {
self.row_translation_map.inner.remove_by_right(&row);
}
// Insert entries for all entries in self.displayed_rows from the start of the replace range.
for rows in &self.displayed_rows[replace_range.start..] {
for row in rows.range.clone() {
self.row_translation_map.inner.insert(row, offset_row);
offset_row += 1;
}
}
}
/// Translates a point from its original location to the displayed point's location (the offset with the filter applied).
///
/// NOTE: If the original location is not found in the row translation map,
/// then it will return the given point.
pub fn maybe_translate_point_from_original_to_displayed(&self, original_point: Point) -> Point {
self.row_translation_map
.maybe_translate_point_from_original_to_displayed(original_point)
}
/// Translates a point from its displayed location (the offset with the filter applied) to its original location.
///
/// NOTE: If the displayed location is not found in the row translation map,
/// then it will return the given point.
pub fn maybe_translate_point_from_displayed_to_original(
&self,
displayed_point: Point,
) -> Point {
self.row_translation_map
.maybe_translate_point_from_displayed_to_original(displayed_point)
}
/// Translates a row from its displayed location (the offset with the filter applied) to its original location.
///
/// NOTE: If the displayed location is not found in the row translation map,
/// then it will return the given row.
pub fn maybe_translate_row_from_displayed_to_original(&self, displayed_row: usize) -> usize {
self.row_translation_map
.maybe_translate_row_from_displayed_to_original(displayed_row)
}
/// Returns true if `row` is a displayed row.
pub fn is_displayed_row(&self, row: usize) -> bool {
self.row_translation_map.is_displayed_row(row)
}
/// If the given original row is being displayed, returns the row it is
/// displayed at. Otherwise, searches for the next closest original row that
/// is greater than the given original row and is being displayed, and
/// returns the row that is displayed at. If there is no next closest row,
/// returns None.
///
/// e.g. If our displayed output structure looked like this:
/// original row | displayed row
/// 2 | 0
/// 3 | 1
/// 7 | 2
/// get_exact_or_next_displayed_row(2) == 0
/// get_exact_or_next_displayed_row(4) == 2
/// get_exact_or_next_displayed_row(8) == None
pub fn get_exact_or_next_displayed_row(&self, target_original_row: usize) -> Option<usize> {
if self.row_translation_map.inner.is_empty() {
return None;
}
if let Some(displayed_row) = self
.row_translation_map
.inner
.get_by_left(&target_original_row)
{
return Some(*displayed_row);
}
// Perform binary search for displayed row of next closest original row.
let mut low = 0;
let mut high = self.row_translation_map.inner.len() - 1;
let mut candidate = None;
while low <= high {
let displayed_row = (high + low) / 2;
let original_row = self
.row_translation_map
.inner
.get_by_right(&displayed_row)?;
match original_row.cmp(&target_original_row) {
Ordering::Greater => {
candidate = Some(displayed_row);
if displayed_row == 0 {
// Break early to avoid underflowing.
break;
}
high = displayed_row - 1;
}
Ordering::Less => {
low = displayed_row + 1;
}
Ordering::Equal => {
// We should never reach this case because of the early exit
// condition at the beginning of the method, but it is included
// here for completeness.
return Some(displayed_row);
}
}
}
candidate
}
/// Finds the first displayed rows that are fully or partially greater than
/// or equal to the given row index.
///
/// Returns the index and a reference to the DisplayedRows object.
///
/// This is mainly used for updating the dirty lines when filtering an active
/// block. The common case has the dirty lines at the end of the block (i.e.
/// adding new lines to the output), so this method performs a reverse linear
/// search to optimize for this case.
pub fn first_rows_greater_than_or_contained_in(
&self,
row: usize,
) -> Option<(usize, &DisplayedRows)> {
let mut start_idx = None;
let mut start_rows = None;
for (idx, rows) in self.displayed_rows.iter().enumerate().rev() {
if row > *rows.range.end() {
break;
}
start_idx = Some(idx);
start_rows = Some(rows);
}
start_idx.zip(start_rows)
}
/// Finds the last displayed rows that are fully or partially less than or
/// equal to the given row index.
///
/// Returns the index and a reference to the DisplayedRows object.
pub fn last_rows_less_than_or_contained_in(
&self,
row: usize,
) -> Option<(usize, &DisplayedRows)> {
self.displayed_rows
.iter()
.enumerate()
.rev()
.find(|(_, rows)| *rows.range.start() <= row)
}
#[cfg(test)]
pub fn new_for_test(displayed_rows: Vec<RangeInclusive<usize>>) -> Self {
DisplayedOutput::new_from_displayed_lines(
displayed_rows
.into_iter()
.map(|rows| DisplayedRows {
range: rows,
source: DisplaySource::FilterMatch,
})
.collect_vec(),
)
}
pub fn reset(&mut self) {
self.displayed_rows.clear();
self.height = 0;
self.row_translation_map.inner.clear();
}
}
#[cfg(test)]
#[path = "displayed_output_test.rs"]
mod tests;
@@ -0,0 +1,717 @@
use super::*;
use bimap::BiMap;
use itertools::Itertools;
/// Converts a vector of ranges into DisplayedRows objects with source FilterMatch.
fn make_displayed_rows_from_ranges(ranges: Vec<RangeInclusive<usize>>) -> Vec<DisplayedRows> {
ranges
.into_iter()
.map(|range| DisplayedRows {
range,
source: DisplaySource::FilterMatch,
})
.collect_vec()
}
#[test]
pub fn test_displayed_output_rows_iterator() {
let displayed_output = DisplayedOutput::new_for_test(vec![0..=2, 4..=4, 7..=10]);
let iterator = displayed_output.rows();
let res = iterator.collect_vec();
assert_eq!(res, vec![0, 1, 2, 4, 7, 8, 9, 10]);
}
#[test]
pub fn test_displayed_output_rows_iterator_no_rows() {
let displayed_output = DisplayedOutput::default();
let mut iterator = displayed_output.rows();
assert!(iterator.next().is_none());
}
#[test]
pub fn test_truncate_to_row() {
let mut displayed_output =
DisplayedOutput::new_for_test(vec![6..=11, 15..=17, 20..=25, 27..=30]);
displayed_output.truncate_to_row(19);
assert_eq!(
displayed_output.displayed_rows,
make_displayed_rows_from_ranges(vec![6..=11, 15..=17])
);
assert_eq!(displayed_output.height, 9);
}
#[test]
pub fn test_truncate_to_row_truncate_before_start() {
let mut displayed_output = DisplayedOutput::new_for_test(vec![6..=11, 12..=14]);
displayed_output.truncate_to_row(5);
assert_eq!(displayed_output.displayed_rows, vec![]);
assert_eq!(displayed_output.height, 0);
}
#[test]
pub fn test_truncate_to_row_truncate_after_end() {
let mut displayed_output = DisplayedOutput::new_for_test(vec![6..=11, 12..=14]);
displayed_output.truncate_to_row(15);
assert_eq!(
displayed_output.displayed_rows,
make_displayed_rows_from_ranges(vec![6..=11, 12..=14])
);
assert_eq!(displayed_output.height, 9);
}
#[test]
pub fn test_truncate_to_row_in_range() {
let mut displayed_output =
DisplayedOutput::new_for_test(vec![6..=11, 15..=17, 20..=25, 27..=30]);
displayed_output.truncate_to_row(16);
assert_eq!(
displayed_output.displayed_rows,
make_displayed_rows_from_ranges(vec![6..=11, 15..=16])
);
assert_eq!(displayed_output.height, 8);
let mut displayed_output =
DisplayedOutput::new_for_test(vec![6..=11, 15..=17, 20..=25, 27..=30]);
displayed_output.truncate_to_row(15);
assert_eq!(
displayed_output.displayed_rows,
make_displayed_rows_from_ranges(vec![6..=11, 15..=15])
);
assert_eq!(displayed_output.height, 7);
let mut displayed_output =
DisplayedOutput::new_for_test(vec![6..=11, 15..=17, 20..=25, 27..=30]);
displayed_output.truncate_to_row(17);
assert_eq!(
displayed_output.displayed_rows,
make_displayed_rows_from_ranges(vec![6..=11, 15..=17])
);
assert_eq!(displayed_output.height, 9);
}
#[test]
pub fn test_truncate_to_row_in_last_range() {
let mut displayed_output =
DisplayedOutput::new_for_test(vec![6..=11, 15..=17, 20..=25, 27..=30]);
displayed_output.truncate_to_row(28);
assert_eq!(
displayed_output.displayed_rows,
make_displayed_rows_from_ranges(vec![6..=11, 15..=17, 20..=25, 27..=28])
);
assert_eq!(displayed_output.height, 17);
}
fn create_bimap_with_insertions(insertion_list: &[(usize, usize)]) -> BiMap<usize, usize> {
let mut bimap = BiMap::new();
for (row, offset_row) in insertion_list {
bimap.insert(*row, *offset_row);
}
bimap
}
#[test]
pub fn test_append_to_row_translation_empty_displayed_rows() {
// Start with empty displayed rows.
let mut displayed_output = DisplayedOutput {
displayed_rows: vec![],
height: 5,
row_translation_map: Default::default(),
};
// Append displayed rows 0..=1.
displayed_output.append_to_row_translation(make_displayed_rows_from_ranges(vec![0..=1]).iter());
let expected_bimap = create_bimap_with_insertions(&[(0, 0), (1, 1)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_append_to_row_translation() {
// Start with displayed rows 0..=1, 4..=5.
let mut bimap = create_bimap_with_insertions(&[(0, 0), (1, 1), (4, 2), (5, 3)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=1, 4..=5]),
height: 5,
row_translation_map: bimap.clone().into(),
};
// Append displayed rows 6..7.
displayed_output.append_to_row_translation(make_displayed_rows_from_ranges(vec![6..=7]).iter());
bimap.insert(6, 4);
bimap.insert(7, 5);
assert_eq!(displayed_output.row_translation_map, bimap.into());
}
#[test]
pub fn test_prepend_to_row_translation_empty_displayed_rows() {
// Start with empty displayed rows.
let mut displayed_output = DisplayedOutput {
displayed_rows: vec![],
height: 5,
row_translation_map: Default::default(),
};
// Prepend displayed rows 0..=1.
let new_line_ranges = make_displayed_rows_from_ranges(vec![0..=1]);
displayed_output
.displayed_rows
.splice(0..0, new_line_ranges);
displayed_output.prepend_to_row_translation();
let mut expected_bimap = BiMap::new();
expected_bimap.insert(0, 0);
expected_bimap.insert(1, 1);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_prepend_to_row_translation() {
// Start with displayed rows 2..=3, 4..=5.
let bimap = create_bimap_with_insertions(&[(2, 0), (3, 1), (4, 2), (5, 3)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![2..=3, 4..=5]),
height: 5,
row_translation_map: bimap.into(),
};
// Prepend displayed rows 0..=1.
let new_line_ranges = make_displayed_rows_from_ranges(vec![0..=1]);
displayed_output
.displayed_rows
.splice(0..0, new_line_ranges);
displayed_output.prepend_to_row_translation();
let expected_bimap =
create_bimap_with_insertions(&[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_replace_rows_from_row_translation() {
// Start with displayed rows 0, 2, 4.
let bimap = create_bimap_with_insertions(&[(0, 0), (2, 1), (4, 2)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=0, 2..=2, 4..=4]),
height: 3,
row_translation_map: bimap.into(),
};
// Replace displayed row 2 with displayed row 3.
let replace_range = 1..2;
let new_line_ranges = make_displayed_rows_from_ranges(vec![3..=3]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[(0, 0), (3, 1), (4, 2)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_replace_rows_from_row_translation_from_middle_to_end() {
// Start with displayed rows 0, 2, 4.
let bimap = create_bimap_with_insertions(&[(0, 0), (2, 1), (4, 2)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=0, 2..=2, 4..=4]),
height: 3,
row_translation_map: bimap.into(),
};
// Replace displayed rows 2, 4 with displayed row 3..=5.
let replace_range = 1..3;
let new_line_ranges = make_displayed_rows_from_ranges(vec![3..=5]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[(0, 0), (3, 1), (4, 2), (5, 3)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_replace_rows_from_row_translation_from_beginning_to_middle() {
// Start with displayed rows 0, 2, 4, 6.
let bimap = create_bimap_with_insertions(&[(0, 0), (2, 1), (4, 2), (6, 3)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=0, 2..=2, 4..=4, 6..=6]),
height: 3,
row_translation_map: bimap.into(),
};
// Replace displayed rows 0, 2 with displayed row 0..=1.
let replace_range = 0..2;
let new_line_ranges = make_displayed_rows_from_ranges(vec![0..=1]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[(0, 0), (1, 1), (4, 2), (6, 3)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_replace_rows_from_row_translation_replace_beginning_row() {
// Start with displayed rows 0.
let bimap = create_bimap_with_insertions(&[(0, 0)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=0]),
height: 1,
row_translation_map: bimap.into(),
};
// Replace displayed rows 0 with displayed row 0.
// This is intended to simulate what happens if we process dirty bytes multiple times.
let replace_range = 0..1;
let new_line_ranges = make_displayed_rows_from_ranges(vec![0..=0]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[(0, 0)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
pub fn test_replace_rows_from_row_translation_break_range() {
// Start with displayed_rows 0..=8.
let bimap = create_bimap_with_insertions((0..=8).map(|x| (x, x)).collect_vec().as_slice());
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=1, 2..=3, 4..=8]),
height: 3,
row_translation_map: bimap.into(),
};
// Replace displayed rows 2..=3 with displayed row 2.
let replace_range = 1..2;
let new_line_ranges = make_displayed_rows_from_ranges(vec![2..=2]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[
(0, 0),
(1, 1),
(2, 2),
(4, 3),
(5, 4),
(6, 5),
(7, 6),
(8, 7),
]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_replace_rows_from_row_translation_no_removed_entries() {
// Start with displayed_rows 0..=1, 6..=7.
let bimap = create_bimap_with_insertions(&[(0, 0), (1, 1), (6, 2), (7, 3)]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![0..=1, 6..=7]),
height: 3,
row_translation_map: bimap.into(),
};
// Insert displayed rows 3..=4.
let replace_range = 1..1;
let new_line_ranges = make_displayed_rows_from_ranges(vec![3..=4]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap =
create_bimap_with_insertions(&[(0, 0), (1, 1), (3, 2), (4, 3), (6, 4), (7, 5)]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_replace_rows_from_row_translation_dirty_range_overlaps_existing_ranges() {
// Start with displayed_rows: [2..=4, 6..=11, 12..=14, 17..=20].
let bimap = create_bimap_with_insertions(&[
(2, 0),
(3, 1),
(4, 2),
(6, 3),
(7, 4),
(8, 5),
(9, 6),
(10, 7),
(11, 8),
(12, 9),
(13, 10),
(14, 11),
(17, 12),
(18, 13),
(19, 14),
(20, 15),
]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![2..=4, 6..=11, 12..=14, 17..=20]),
height: 0,
row_translation_map: bimap.into(),
};
// Pretend that the dirty byte range is 8..=13.
// The new displayed rows for the dirty range are 8..=10, 12..=12.
let replace_range = 1..3;
let new_line_ranges = make_displayed_rows_from_ranges(vec![8..=10, 12..=12]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[
(2, 0),
(3, 1),
(4, 2),
(8, 3),
(9, 4),
(10, 5),
(12, 6),
(17, 7),
(18, 8),
(19, 9),
(20, 10),
]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_replace_rows_from_row_translation_dirty_range_lies_within_existing_range() {
// Start with displayed_rows [2..=4, 6..=15, 17..=20].
let bimap = create_bimap_with_insertions(&[
(2, 0),
(3, 1),
(4, 2),
(6, 3),
(7, 4),
(8, 5),
(9, 6),
(10, 7),
(11, 8),
(12, 9),
(13, 10),
(14, 11),
(15, 12),
(17, 13),
(18, 14),
(19, 15),
(20, 16),
]);
let mut displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![2..=4, 6..=15, 17..=20]),
height: 0,
row_translation_map: bimap.into(),
};
// Pretend that the dirty byte range is 8..=13.
// The new displayed rows for the dirty range are 8..=10, 12..=12.
let replace_range = 1..2;
let new_line_ranges = make_displayed_rows_from_ranges(vec![6..=10, 12..=12, 13..=15]);
displayed_output
.displayed_rows
.splice(replace_range.clone(), new_line_ranges);
displayed_output.replace_rows_from_row_translation(replace_range);
let expected_bimap = create_bimap_with_insertions(&[
(2, 0),
(3, 1),
(4, 2),
(6, 3),
(7, 4),
(8, 5),
(9, 6),
(10, 7),
(12, 8),
(13, 9),
(14, 10),
(15, 11),
(17, 12),
(18, 13),
(19, 14),
(20, 15),
]);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_get_exact_or_next_displayed_row() {
// Start with displayed_rows [2..=3, 6..=8, 14..=15].
let bimap =
create_bimap_with_insertions(&[(2, 0), (3, 1), (6, 2), (7, 3), (8, 4), (14, 5), (15, 6)]);
let displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![2..=3, 6..=8, 14..=15]),
height: 0,
row_translation_map: bimap.into(),
};
let expected_input_output_pairs = vec![
(0, Some(0)),
(1, Some(0)),
(2, Some(0)),
(3, Some(1)),
(4, Some(2)),
(5, Some(2)),
(6, Some(2)),
(7, Some(3)),
(8, Some(4)),
(9, Some(5)),
(10, Some(5)),
(11, Some(5)),
(12, Some(5)),
(13, Some(5)),
(14, Some(5)),
(15, Some(6)),
];
for (input, expected_output) in expected_input_output_pairs {
assert_eq!(
displayed_output.get_exact_or_next_displayed_row(input),
expected_output
);
}
}
#[test]
fn test_get_exact_or_next_displayed_row_no_next_closest() {
// Start with displayed_rows [2..=3, 6..=8, 14..=15].
let bimap =
create_bimap_with_insertions(&[(2, 0), (3, 1), (6, 2), (7, 3), (8, 4), (14, 5), (15, 6)]);
let displayed_output = DisplayedOutput {
displayed_rows: make_displayed_rows_from_ranges(vec![2..=3, 6..=8, 14..=15]),
height: 0,
row_translation_map: bimap.into(),
};
assert_eq!(displayed_output.get_exact_or_next_displayed_row(16), None);
}
#[test]
fn test_get_exact_or_next_displayed_row_empty() {
let displayed_output = DisplayedOutput {
displayed_rows: Vec::new(),
height: 0,
row_translation_map: BiMap::new().into(),
};
assert!(displayed_output
.get_exact_or_next_displayed_row(5)
.is_none());
}
#[test]
fn test_new_from_displayed_lines() {
let displayed_rows = make_displayed_rows_from_ranges(vec![1..=3, 7..=7, 14..=18]);
let displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let expected_bimap = create_bimap_with_insertions(&[
(1, 0),
(2, 1),
(3, 2),
(7, 3),
(14, 4),
(15, 5),
(16, 6),
(17, 7),
(18, 8),
]);
assert_eq!(displayed_output.displayed_rows, displayed_rows);
assert_eq!(displayed_output.height, 9);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_extend_displayed_lines() {
let displayed_rows = make_displayed_rows_from_ranges(vec![1..=3]);
let mut displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
displayed_output.extend_displayed_lines(make_displayed_rows_from_ranges(vec![7..=7, 14..=18]));
let expected_displayed_rows = make_displayed_rows_from_ranges(vec![1..=3, 7..=7, 14..=18]);
let expected_bimap = create_bimap_with_insertions(&[
(1, 0),
(2, 1),
(3, 2),
(7, 3),
(14, 4),
(15, 5),
(16, 6),
(17, 7),
(18, 8),
]);
assert_eq!(displayed_output.displayed_rows, expected_displayed_rows);
assert_eq!(displayed_output.height, 9);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_prepend_displayed_lines() {
let displayed_rows = make_displayed_rows_from_ranges(vec![14..=18]);
let mut displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
displayed_output.prepend_displayed_lines(make_displayed_rows_from_ranges(vec![1..=3, 7..=7]));
let expected_displayed_rows = make_displayed_rows_from_ranges(vec![1..=3, 7..=7, 14..=18]);
let expected_bimap = create_bimap_with_insertions(&[
(1, 0),
(2, 1),
(3, 2),
(7, 3),
(14, 4),
(15, 5),
(16, 6),
(17, 7),
(18, 8),
]);
assert_eq!(displayed_output.displayed_rows, expected_displayed_rows);
assert_eq!(displayed_output.height, 9);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_splice_displayed_lines() {
let displayed_rows = make_displayed_rows_from_ranges(vec![0..=1, 4..=5, 14..=16, 19..=19]);
let mut displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let new_displayed_rows = make_displayed_rows_from_ranges(vec![6..=6, 8..=9]);
displayed_output.splice_displayed_lines(1..3, new_displayed_rows);
let expected_displayed_rows =
make_displayed_rows_from_ranges(vec![0..=1, 6..=6, 8..=9, 19..=19]);
let expected_bimap =
create_bimap_with_insertions(&[(0, 0), (1, 1), (6, 2), (8, 3), (9, 4), (19, 5)]);
assert_eq!(displayed_output.displayed_rows, expected_displayed_rows);
assert_eq!(displayed_output.height, 6);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
let new_displayed_rows = make_displayed_rows_from_ranges(vec![10..=12, 14..=15]);
displayed_output.splice_displayed_lines(3..4, new_displayed_rows);
let expected_displayed_rows =
make_displayed_rows_from_ranges(vec![0..=1, 6..=6, 8..=9, 10..=12, 14..=15]);
let expected_bimap = create_bimap_with_insertions(&[
(0, 0),
(1, 1),
(6, 2),
(8, 3),
(9, 4),
(10, 5),
(11, 6),
(12, 7),
(14, 8),
(15, 9),
]);
assert_eq!(displayed_output.displayed_rows, expected_displayed_rows);
assert_eq!(displayed_output.height, 10);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_splice_displayed_lines_nothing_replaced() {
let displayed_rows = make_displayed_rows_from_ranges(vec![0..=1, 4..=5, 14..=16, 19..=19]);
let mut displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let new_displayed_rows = make_displayed_rows_from_ranges(vec![8..=9]);
displayed_output.splice_displayed_lines(2..2, new_displayed_rows);
let expected_displayed_rows =
make_displayed_rows_from_ranges(vec![0..=1, 4..=5, 8..=9, 14..=16, 19..=19]);
let expected_bimap = create_bimap_with_insertions(&[
(0, 0),
(1, 1),
(4, 2),
(5, 3),
(8, 4),
(9, 5),
(14, 6),
(15, 7),
(16, 8),
(19, 9),
]);
assert_eq!(displayed_output.displayed_rows, expected_displayed_rows);
assert_eq!(displayed_output.height, 10);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_splice_displayed_lines_no_new_lines() {
let displayed_rows = make_displayed_rows_from_ranges(vec![0..=1, 4..=5, 14..=16, 19..=19]);
let mut displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
displayed_output.splice_displayed_lines(1..2, Vec::new());
let expected_displayed_rows = make_displayed_rows_from_ranges(vec![0..=1, 14..=16, 19..=19]);
let expected_bimap =
create_bimap_with_insertions(&[(0, 0), (1, 1), (14, 2), (15, 3), (16, 4), (19, 5)]);
assert_eq!(displayed_output.displayed_rows, expected_displayed_rows);
assert_eq!(displayed_output.height, 6);
assert_eq!(displayed_output.row_translation_map, expected_bimap.into());
}
#[test]
fn test_first_rows_greater_than_or_contained_in() {
let displayed_rows = make_displayed_rows_from_ranges(vec![2..=5, 7..=9, 14..=16, 19..=19]);
let displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let res = displayed_output.first_rows_greater_than_or_contained_in(19);
assert_eq!(res.unwrap().0, 3);
assert_eq!(res.unwrap().1.range.clone(), 19..=19);
let res = displayed_output.first_rows_greater_than_or_contained_in(15);
assert_eq!(res.unwrap().0, 2);
assert_eq!(res.unwrap().1.range.clone(), 14..=16);
let res = displayed_output.first_rows_greater_than_or_contained_in(1);
assert_eq!(res.unwrap().0, 0);
assert_eq!(res.unwrap().1.range.clone(), 2..=5);
}
#[test]
fn test_first_rows_greater_than_or_contained_in_no_result() {
let displayed_rows = make_displayed_rows_from_ranges(vec![2..=5, 7..=9, 14..=16, 19..=19]);
let displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let res = displayed_output.first_rows_greater_than_or_contained_in(20);
assert_eq!(res, None);
}
#[test]
fn test_last_rows_less_than_or_contained_in() {
let displayed_rows = make_displayed_rows_from_ranges(vec![2..=5, 7..=9, 14..=16, 19..=19]);
let displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let res = displayed_output.last_rows_less_than_or_contained_in(2);
assert_eq!(res.unwrap().0, 0);
assert_eq!(res.unwrap().1.range.clone(), 2..=5);
let res = displayed_output.last_rows_less_than_or_contained_in(15);
assert_eq!(res.unwrap().0, 2);
assert_eq!(res.unwrap().1.range.clone(), 14..=16);
let res = displayed_output.last_rows_less_than_or_contained_in(20);
assert_eq!(res.unwrap().0, 3);
assert_eq!(res.unwrap().1.range.clone(), 19..=19);
}
#[test]
fn test_last_rows_less_than_or_contained_in_no_result() {
let displayed_rows = make_displayed_rows_from_ranges(vec![2..=5, 7..=9, 14..=16, 19..=19]);
let displayed_output = DisplayedOutput::new_from_displayed_lines(displayed_rows.clone());
let res = displayed_output.last_rows_less_than_or_contained_in(1);
assert_eq!(res, None);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,283 @@
use std::borrow::Cow;
use warp_terminal::model::grid::{
cell::{self, Cell},
row::Row,
CellType,
};
use crate::terminal::model::index::Point;
use super::{grid_handler::GridHandler, CursorDirection, CursorState, Dimensions as _};
/// The set of possible grapheme cursor wrapping behaviors.
#[derive(PartialEq)]
pub enum Wrap {
/// Does not wrap at all (stops at the start and end of a row).
None,
/// Wraps at the start/end of a row, but stops upon reaching a newline.
Soft,
/// Wraps at the start and end of each row, ignoring newlines.
All,
}
/// A cursor for iterating forward or backward over graphemes in a terminal
/// grid.
///
/// If constructed on a wide char spacer cell, the cursor will be moved one
/// cell to the left, snapping it to the cell which contains the content for
/// the grapheme under the cursor. If constructed on a leading wide char
/// spacer cell, the cursor will be moved one cell to the right (for the same
/// reason).
pub struct GraphemeCursor<'g> {
grid: &'g GridHandler,
cur: Point,
cursor_state: CursorState,
wrap: Wrap,
/// The index of the row that is stored in `cached_row`.
cached_row_idx: usize,
/// A cached shared reference to the row with index `cached_row_idx`.
///
/// This is a `Cow` because grid storage internally holds `Row`s, and can
/// return a reference to an existing one, whereas flat storage needs to
/// construct one on-demand from its internal representation.
cached_row: Option<Cow<'g, Row>>,
}
impl<'g> GraphemeCursor<'g> {
/// Returns a new grapheme cursor that starts at the given point and
/// adheres to the provided wrapping behavior.
pub fn new(point: Point, grid: &'g GridHandler, wrap: Wrap) -> Self {
if let Some(row) = grid.row(point.row) {
if let Some(cell) = row.get(point.col) {
let flags = cell.flags;
let mut cursor = Self {
grid,
cur: point,
cursor_state: CursorState::Valid,
wrap,
cached_row_idx: point.row,
cached_row: Some(row),
};
// The cursor should never start on a spacer cell. If we're on
// the second cell in a wide char, move to the first cell. If
// we're on the cell where a wide char _should_ have started,
// move to its actual start cell.
if flags.intersects(cell::Flags::WIDE_CHAR_SPACER) {
cursor.move_backward();
} else if flags.intersects(cell::Flags::LEADING_WIDE_CHAR_SPACER) {
cursor.move_forward();
}
return cursor;
}
}
Self {
grid,
cur: point,
cursor_state: CursorState::Invalid,
wrap,
cached_row_idx: point.row,
cached_row: None,
}
}
/// Returns a struct that provides access to information about the grapheme
/// under the cursor, or [`None`] if the cursor is invalid or exhausted.
pub fn current_item(&self) -> Option<GraphemeCursorItem<'_>> {
match self.cursor_state {
CursorState::Valid => {
let cell = self
.cached_row
.as_ref()
.expect("row should be valid")
.get(self.cur.col)
.expect("col should be valid");
Some(GraphemeCursorItem {
cell,
point: self.cur,
})
}
_ => None,
}
}
/// Returns the position the cursor had when it was last valid.
///
/// This is useful for knowing the "final" cursor position after it has
/// been exhausted (e.g.: hitting the start/end of the grid or a wrapping
/// boundary condition).
pub fn last_valid_position(&self) -> Point {
self.cur
}
fn current_point_valid(&self) -> bool {
self.cur.row < self.grid.total_rows() && self.cur.col < self.grid.columns()
}
fn has_next(&self) -> bool {
if !self.current_point_valid() {
return false;
}
let at_end_of_row = self.is_at_end_of_row();
if self.wrap == Wrap::None && at_end_of_row
|| self.wrap == Wrap::Soft && self.is_at_end_of_line()
{
return false;
}
!at_end_of_row || self.cur.row < self.grid.total_rows() - 1
}
fn has_prev(&self) -> bool {
if !self.current_point_valid() {
return false;
}
let at_start_of_row = self.is_at_start_of_row();
if self.wrap == Wrap::None && at_start_of_row
|| self.wrap == Wrap::Soft && self.is_at_start_of_line()
{
return false;
}
!at_start_of_row || self.cur.row > 0
}
fn update_cached_row(&mut self) {
self.cached_row_idx = self.cur.row;
self.cached_row = self.grid.row(self.cached_row_idx);
}
/// Moves the cursor forward by a single grapheme.
pub fn move_forward(&mut self) {
match self.cursor_state {
CursorState::Valid if self.has_next() => {
let start_row = self.cur.row;
self.cur = self.cur.wrapping_add(self.grid.columns(), 1);
// Skip over any spacer cells.
if matches!(
self.grid.cell_type(self.cur),
Some(CellType::WideCharSpacer | CellType::LeadingWideCharSpacer)
) {
self.move_forward();
}
if self.cur.row != start_row {
self.update_cached_row();
}
}
CursorState::Valid => {
self.cursor_state = CursorState::Exhausted(CursorDirection::Right);
}
CursorState::Exhausted(CursorDirection::Left) => {
self.cursor_state = CursorState::Valid;
}
_ => (),
}
}
/// Moves the cursor backward by a single grapheme.
pub fn move_backward(&mut self) {
match self.cursor_state {
CursorState::Valid if self.has_prev() => {
let start_row = self.cur.row;
self.cur = self.cur.wrapping_sub(self.grid.columns(), 1);
// Skip over any spacer cells.
if matches!(
self.grid.cell_type(self.cur),
Some(CellType::WideCharSpacer | CellType::LeadingWideCharSpacer)
) {
self.move_backward();
}
if self.cur.row != start_row {
self.update_cached_row();
}
}
CursorState::Valid => {
self.cursor_state = CursorState::Exhausted(CursorDirection::Left);
}
CursorState::Exhausted(CursorDirection::Right) => {
self.cursor_state = CursorState::Valid;
}
_ => (),
}
}
fn is_at_start_of_row(&self) -> bool {
self.cur.col == 0
}
fn is_at_end_of_row(&self) -> bool {
self.cur.col == self.grid.columns() - 1
}
/// Returns whether or not the cursor is at the start of a line.
///
/// In other words, this returns true if moving the cursor backwards would
/// transition across a newline/hard wrap.
pub fn is_at_start_of_line(&self) -> bool {
self.is_at_start_of_row() && (self.cur.row == 0 || !self.grid.row_wraps(self.cur.row - 1))
}
/// Returns whether or not the cursor is at the end of a line.
///
/// In other words, this returns true if moving the cursor forwards would
/// transition across a newline/hard wrap.
pub fn is_at_end_of_line(&self) -> bool {
self.is_at_end_of_row()
&& (self.cur.row == self.grid.total_rows() - 1 || !self.grid.row_wraps(self.cur.row))
}
}
/// A helper struct for providing information about the grapheme at which the
/// cursor is currently located.
pub struct GraphemeCursorItem<'g> {
cell: &'g Cell,
point: Point,
}
impl GraphemeCursorItem<'_> {
/// Returns a reference to the cell that holds the content for the grapheme
/// under the cursor.
///
/// TODO(CORE-2955): Fix the fact that many callers look at `cell().c` to
/// get the cell content, which is incorrect for cells
/// which have additional content in `CellExtra`.
pub fn cell(&self) -> &Cell {
self.cell
}
/// Returns the character in the cell under the grapheme cursor.
///
/// TODO(CORE-2955): Fix the fact that many callers look at `cell().c` to
/// get the cell content, which is incorrect for cells
/// which have additional content in `CellExtra`.
pub fn content_char(&self) -> char {
if self.cell.c == cell::DEFAULT_CHAR {
' '
} else {
self.cell.c
}
}
/// Returns the item's position in the grid.
pub fn point(&self) -> Point {
self.point
}
}
#[cfg(test)]
#[path = "grapheme_cursor_tests.rs"]
mod tests;
@@ -0,0 +1,71 @@
use super::*;
fn cell(c: char) -> Cell {
let mut cell = Cell::default();
cell.c = c;
cell
}
#[test]
fn test_cursor() {
macro_rules! assert_cursor_contents_eq {
($c:expr, $cursor:ident) => {
let item = $cursor
.current_item()
.expect("cursor location should be valid");
assert_eq!(&cell($c), item.cell());
};
}
let mut grid = GridHandler::new_for_test(5, 5);
for i in 0u8..5u8 {
for j in 0u8..5u8 {
grid.grid_storage_mut()[usize::from(i)][usize::from(j)] = cell((i * 5 + j) as char);
}
}
let mut cursor = grid.grapheme_cursor_from(Point { row: 0, col: 0 }, Wrap::All);
cursor.move_backward();
assert!(cursor.current_item().is_none());
cursor.move_forward();
assert_cursor_contents_eq!(0u8 as char, cursor);
cursor.move_forward();
assert_cursor_contents_eq!(1u8 as char, cursor);
assert_eq!(Some(1), cursor.current_item().map(|item| item.point().col));
assert_eq!(Some(0), cursor.current_item().map(|item| item.point().row));
cursor.move_forward();
assert_cursor_contents_eq!(2u8 as char, cursor);
cursor.move_forward();
assert_cursor_contents_eq!(3u8 as char, cursor);
cursor.move_forward();
assert_cursor_contents_eq!(4u8 as char, cursor);
// Test line-wrapping.
cursor.move_forward();
assert_cursor_contents_eq!(5u8 as char, cursor);
assert_eq!(Some(0), cursor.current_item().map(|item| item.point().col));
assert_eq!(Some(1), cursor.current_item().map(|item| item.point().row));
cursor.move_backward();
assert_cursor_contents_eq!(4u8 as char, cursor);
assert_eq!(Some(4), cursor.current_item().map(|item| item.point().col));
assert_eq!(Some(0), cursor.current_item().map(|item| item.point().row));
// Make sure iter.cell() returns the current iterator position.
assert_cursor_contents_eq!(4u8 as char, cursor);
// Test that iter ends at end of grid.
let mut final_cursor = grid.grapheme_cursor_from(Point { row: 4, col: 4 }, Wrap::All);
final_cursor.move_forward();
assert!(final_cursor.current_item().is_none());
final_cursor.move_backward();
assert_cursor_contents_eq!(24u8 as char, final_cursor);
final_cursor.move_backward();
assert_cursor_contents_eq!(23u8 as char, final_cursor);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+597
View File
@@ -0,0 +1,597 @@
//! A specialized 2D grid implementation optimized for use in a terminal.
mod resize;
use std::cmp::min;
use std::ops::{Index, IndexMut, Range};
use serde::{Deserialize, Serialize};
pub use warp_terminal::model::grid::Dimensions;
use crate::features::FeatureFlag;
use crate::terminal::model::ansi::{CharsetIndex, StandardCharset};
use crate::terminal::model::cell::{Cell, Flags};
use crate::terminal::model::grid::row::Row;
use crate::terminal::model::grid::storage::Storage;
use crate::terminal::model::index::{IndexRange, Point, VisiblePoint, VisibleRow};
use crate::terminal::model::secrets::ObfuscateSecrets;
impl ::std::cmp::PartialEq for GridStorage {
fn eq(&self, other: &Self) -> bool {
// Compare struct fields and check result of grid comparison.
self.raw.eq(&other.raw) && self.columns.eq(&other.columns) && self.rows.eq(&other.rows)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Cursor {
/// The location of this cursor.
pub point: VisiblePoint,
/// Template cell when using this cursor.
pub template: Cell,
/// Currently configured graphic character sets.
pub charsets: Charsets,
/// Tracks if the next call to input will need to first handle wrapping.
///
/// This is true after the last column is set with the input function. Any function that
/// implicitly sets the line or column needs to set this to false to avoid wrapping twice.
///
/// Tracking `input_needs_wrap` makes it possible to not store a cursor position that exceeds
/// the number of columns, which would lead to index out of bounds when interacting with arrays
/// without sanitization.
pub input_needs_wrap: bool,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct Charsets([StandardCharset; 4]);
impl Index<CharsetIndex> for Charsets {
type Output = StandardCharset;
fn index(&self, index: CharsetIndex) -> &StandardCharset {
&self.0[index as usize]
}
}
impl IndexMut<CharsetIndex> for Charsets {
fn index_mut(&mut self, index: CharsetIndex) -> &mut StandardCharset {
&mut self.0[index as usize]
}
}
/// Grid based terminal content storage.
///
/// The grid is a 0-based buffer that goes from index 0 to however large the grid needs to be.
///
/// ┌────────────────────────────────┐
/// 0:│cat best_shakespeare_plays.txt │ <-- command grid
/// ├────────────────────────────────┤
/// 0:│Hamlet │
/// 1:│Othello │
/// 2:│Macbeth │ <-- output grid
/// 3:│The Tempest │
/// 4:│Pericles │
/// └────────────────────────────────┘
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GridStorage {
/// Current cursor for writing data.
#[serde(skip)]
pub(super) cursor: Cursor,
/// The maximum line and column the cursor has been on.
#[serde(skip)]
pub max_cursor_point: VisiblePoint,
/// Last saved cursor.
#[serde(skip)]
pub saved_cursor: Cursor,
/// VisibleRows in the grid. Each row holds a list of cells corresponding to the
/// columns in that row.
pub(super) raw: Storage,
/// Number of columns.
pub(crate) columns: usize,
/// Number of visible rows.
pub(crate) rows: usize,
/// Maximum number of lines in history.
pub(crate) max_scroll_limit: usize,
#[serde(skip)]
/// The number of lines that have been truncated due
/// to the [`Grid::max_scroll_limit`].
///
/// This is specifically of type [`u64`] to ensure
/// that we don't overflow for blocks with a lot of truncation.
/// We might want to consider using a smalluint if this becomes a problem.
pub(super) num_lines_truncated: u64,
/// Whether the resize fix feature flag is enabled (Alacritty cursor reflow bug during resize). Gating
/// fix to gain confidence in the fix before enabling for all users.
#[serde(skip)]
pub(crate) resize_fix_ff_enabled: bool,
}
impl GridStorage {
pub fn new(
rows: usize,
columns: usize,
max_scroll_limit: usize,
// TODO(vorporeal): remove this argument entirely
_secret_obfuscation_mode: ObfuscateSecrets,
) -> GridStorage {
GridStorage {
raw: Storage::with_capacity(rows, columns, FeatureFlag::SequentialStorage.is_enabled()),
max_scroll_limit,
saved_cursor: Cursor::default(),
cursor: Cursor::default(),
max_cursor_point: Default::default(),
rows,
columns,
resize_fix_ff_enabled: FeatureFlag::ResizeFix.is_enabled(),
num_lines_truncated: 0,
}
}
/// Constructs a new [`GridStorage`] with a subset of rows from `self`.
///
/// `num_preceding_rows` is the number of rows in `self` that come before
/// the rows in `rows`.
///
/// `initial_history_size` is the number of rows in the parent
/// [`GridHandler`] that are in scrollback. This is needed to properly
/// convert [`VisiblePoint`]s to [`Point`]s.
pub(in crate::terminal::model) fn new_for_split(
&self,
mut rows: Vec<Row>,
num_preceding_rows: usize,
initial_history_size: usize,
) -> GridStorage {
let visible_lines = rows.len();
// If we're not using sequential storage, we store the rows in reverse
// order, so reverse them here.
if !self.raw.is_sequential() {
rows.reverse();
}
let mut grid = GridStorage {
cursor: self.cursor.clone(),
max_cursor_point: self.max_cursor_point,
saved_cursor: self.saved_cursor.clone(),
raw: Storage::with_rows(rows, self.raw.is_sequential(), visible_lines),
columns: self.columns,
rows: visible_lines,
max_scroll_limit: self.max_scroll_limit,
resize_fix_ff_enabled: self.resize_fix_ff_enabled,
num_lines_truncated: 0,
};
let new_history_size = grid.history_size();
let new_visible_rows = grid.visible_rows();
let columns = grid.columns();
let adjust_cursor_point = |cursor_point: &mut VisiblePoint| {
// Convert the cursor row to absolute coordinates, using the
// pre-split history size from the `GridHandler` level (so that we
// account for rows in flat storage).
let row = cursor_point.row.0 + initial_history_size;
// Check if the cursor is within this grid or before it.
if let Some(row) = row.checked_sub(num_preceding_rows) {
let visible_row = row.saturating_sub(new_history_size);
if visible_row < new_visible_rows {
// If the cursor is within this grid, update the row
// accordingly.
cursor_point.row = VisibleRow(visible_row);
} else {
// If it is past the end of the grid, put it at the end.
cursor_point.row = VisibleRow(new_visible_rows - 1);
cursor_point.col = columns - 1;
}
} else {
// The cursor is before this grid, so put it at the start of
// the grid.
cursor_point.row = VisibleRow(0);
cursor_point.col = 0;
}
};
adjust_cursor_point(&mut grid.cursor.point);
adjust_cursor_point(&mut grid.saved_cursor.point);
adjust_cursor_point(&mut grid.max_cursor_point);
grid
}
pub(super) fn set_stored_rows(
&mut self,
mut rows: Vec<Row>,
visible_rows: usize,
columns: usize,
) {
// Ensure there are a full set of rows.
rows.resize_with(visible_rows, || Row::new(columns));
debug_assert_eq!(rows.len(), visible_rows);
self.rows = visible_rows;
self.columns = columns;
let is_sequential = self.raw.is_sequential();
if !is_sequential {
rows.reverse();
}
self.raw = Storage::with_rows(rows, is_sequential, visible_rows);
}
/// Update the size of the scrollback history.
pub fn update_history(&mut self, history_size: usize) {
let current_history_size = self.history_size();
if current_history_size > history_size {
self.raw.shrink_lines(current_history_size - history_size);
}
self.max_scroll_limit = history_size;
}
fn increase_scroll_limit(&mut self, count: usize) {
let count = min(count, self.max_scroll_limit - self.history_size());
if count != 0 {
self.raw.initialize(count, self.columns);
}
}
pub(crate) fn decrease_scroll_limit(&mut self, count: usize) {
let count = min(count, self.history_size());
if count != 0 {
self.raw.shrink_lines(min(count, self.history_size()));
}
}
pub fn update_max_cursor(&mut self) {
let point = self.cursor.point;
if point.row.0 >= self.rows {
return;
}
if point.row > self.max_cursor_point.row
|| (point.row == self.max_cursor_point.row && point.col > self.max_cursor_point.col)
{
self.max_cursor_point = point;
}
}
/// Returns an immutable reference to the active [`Cursor`]. See [`Self::update_cursor`] as an
/// alternative that provides mutable access to the `Cursor`.
pub fn cursor(&self) -> &Cursor {
&self.cursor
}
pub(super) fn row_wraps(&self, row_idx: usize) -> bool {
let Some(cell) = self.get(row_idx).and_then(|row| row.last()) else {
return false;
};
cell.flags().contains(Flags::WRAPLINE)
}
/// Moves everything in the visible screen down by "positions" amount of rows.
/// Importantly, this is the concept of grid scrolling, not blocklist scrolling. Examples
/// of operations that use this method are any alt-screen command, git log, or git diff.
///
/// The "region" parameter is a subset of the VisibleScreen, and it tells us about any fixed lines
/// at the top or bottom of the screen that should be excluded from the scroll action. For example,
/// the VisibleScreen might be (0..40) and the region might be (1..40), indicating that there's
/// one fixed line at the top of the grid.
///
/// Say the grid has 10 rows (0..9) and visible_rows = grid.len() (i.e., no scrollback).
/// If there's one fixed line at the bottom of the grid and we scroll down by one line...
/// (1) Swap the fixed line at the bottom with the line above it. (ix 9 <--> ix 8)
/// (2) Inform the storage layer of the change to the bottommost row. ("shift up by one")
/// (3) Clear the topmost row (ix 0).
///
/// If there is a scrollback, this process requires piecewise swapping of the visible rows.
/// For a more detailed specification, look at test_grid_scroll_down() and test_grid_git_diff_or_log()
#[inline]
pub fn scroll_down(&mut self, region: &Range<VisibleRow>, scroll_distance: usize) {
let visible_rows = self.visible_rows();
// When rotating the entire region, just reset everything.
if scroll_distance >= region.end - region.start {
for line in IndexRange(region.start..region.end) {
self.raw[line].reset(&self.cursor.template);
}
return;
}
// Which implementation we can use depends on the existence of a scrollback history.
//
// Since a scrollback history prevents us from rotating the entire buffer downwards, we
// instead have to rely on a slower, swap-based implementation.
if self.max_scroll_limit == 0 {
// Swap the lines fixed at the bottom to their target positions after rotation.
//
// Since we've made sure that the rotation will never rotate away the entire region, we
// know that the position of the fixed lines before the rotation must already be
// visible.
//
// We need to start from the top, to make sure the fixed lines aren't swapped with each
// other.
let fixed_lines = visible_rows - region.end.0;
for i in (0..fixed_lines).rev() {
let index = visible_rows - i - 1;
self.raw.swap(index, index - scroll_distance);
}
// Reduce the bottommost row of the grid. In other words, retract the concept of the bottom_row
// by some integer value.
self.raw.retract(scroll_distance as isize);
// The new lines appear at the top of the screen. Reset them.
for i in 0..scroll_distance {
self.raw[i].reset(&self.cursor.template);
}
// Swap the fixed lines at the top back into position by swapping them upwards.
for i in 0..region.start.0 {
self.raw.swap(i, i + scroll_distance);
}
} else {
// Subregion rotation is a two-step process of bubbling the bottom rows to the top
// and then clearing them. Like so:
// 0: a 0: e 0:
// 1: b swap 1: a clear 1: a
// 2: c ----> 2: b ----> 2: b
// 3: d 3: c 3: c
// 4: e 4: d 4: d
// Starting with the bottommost visible row, swap everything up by one, resulting in
// the bottommost row ending up as the topmost row and everything else shifted down by one.
for line in IndexRange((region.start + scroll_distance)..region.end).rev() {
self.raw.swap_lines(line, line - scroll_distance);
}
// Clear rows at the top, including the rows we just swapped there.
for line in IndexRange(region.start..(region.start + scroll_distance)) {
self.raw[line].reset(&self.cursor.template);
}
}
}
/// Moves everything in the visible screen up by `scroll_distance` number of rows.
/// Importantly, this is the concept of grid scrolling, not blocklist scrolling. Examples
/// of operations that use this method are any alt-screen command, git log, or git diff.
///
/// This is the performance-sensitive part of scrolling!
///
/// For a more detailed specification, look at test_grid_scroll_up() and test_grid_git_diff_or_log()
pub fn scroll_up(&mut self, region: &Range<VisibleRow>, scroll_distance: usize) {
let visible_rows = self.visible_rows();
// When rotating the entire region with fixed lines at the top, just reset everything.
if scroll_distance >= region.end - region.start && region.start != VisibleRow(0) {
for line in IndexRange(region.start..region.end) {
self.raw[line].reset(&self.cursor.template);
}
return;
}
// Before extending the scrollback by the scroll amount, calculate
// how many lines are going to be truncated, if any.
let num_lines_to_truncate =
(self.history_size() + scroll_distance).saturating_sub(self.max_scroll_limit);
// Create scrollback for the new lines.
self.increase_scroll_limit(scroll_distance);
// Swap the lines fixed at the top to their target positions after rotation.
//
// Since we've made sure that the rotation will never rotate away the entire region, we
// know that the position of the fixed lines before the rotation must already be
// visible.
//
// We need to start from the bottom, to make sure the fixed lines aren't swapped with each
// other.
for i in (0..region.start.0).rev() {
self.raw.swap(i, i + scroll_distance);
}
// Mark that the bottommost row is now extended.
self.raw.extend(scroll_distance as isize);
// The new lines appear at the bottom of the screen. Reset them.
for i in self.raw.len() - scroll_distance..self.raw.len() {
self.raw[i].reset(&self.cursor.template);
}
// Swap the fixed lines at the bottom back into position.
let fixed_lines = VisibleRow(visible_rows) - region.end;
for i in 0..fixed_lines {
let index = visible_rows - i - 1;
self.raw.swap(index, index - scroll_distance);
}
// After modifying the scrollback buffer, check if we're truncating
// rows and keep track of how many rows we've truncated.
if self.max_scroll_limit == self.history_size() {
self.num_lines_truncated += num_lines_to_truncate as u64;
}
}
/// Clear the grid, leaving the cursor's line at the top and preserving the cursor location
/// within the line.
pub(super) fn clear_and_reset_saving_cursor_line(&mut self) {
self.clear_history();
self.raw.swap_lines(self.cursor.point.row, VisibleRow(0));
self.cursor.point.row = VisibleRow(0);
// Reset all visible lines except the top most row.
for row in 1..self.raw.len() {
self.raw[row].reset(&Cell::default());
}
}
/// Completely reset the grid state.
pub fn reset(&mut self) {
self.clear_history();
self.saved_cursor = Cursor::default();
self.cursor = Cursor::default();
self.max_cursor_point = Default::default();
// Reset all visible lines.
for row in 0..self.raw.len() {
self.raw[row].reset(&self.cursor.template);
}
}
/// Populate grid from 2D char array - particularly useful for testing purposes.
#[cfg(test)]
pub fn populate_from_array(&mut self, arr: &[&[char]]) {
for (i, row) in arr.iter().enumerate() {
for (j, &cell) in row.iter().enumerate() {
if i < self.rows && j < self.columns {
self[i][j].c = cell;
}
}
}
}
pub fn estimated_memory_usage_bytes(&self) -> usize {
std::mem::size_of_val(self) + self.estimated_heap_usage_bytes()
}
pub fn estimated_heap_usage_bytes(&self) -> usize {
// For right now, we're only factoring in the heap size of
// the grid storage. Some other fields (e.g.: `secrets` and
// `secrets_in_plaintext`) contain heap-allocated data, but we
// aren't as worried about them for now.
self.raw.estimated_heap_usage_bytes()
}
}
#[allow(clippy::len_without_is_empty)]
impl GridStorage {
#[inline]
pub fn clear_history(&mut self) {
// Explicitly purge all lines from history.
self.raw.shrink_lines(self.history_size());
}
/// The number of rows in the grid up until the cursor.
fn rows_to_cursor(&self) -> usize {
self.cursor.point.row.0 + self.history_size()
}
/// The number of visible rows in the grid up until the cursor.
pub fn visible_rows_to_cursor(&self) -> usize {
self.cursor.point.row.0
}
/// The number of columns in the grid up until the cursor.
pub fn cols_to_cursor(&self) -> usize {
self.cursor.point.col
}
/// Truncate all rows after the cursor's row from the Grid.
#[inline]
pub(super) fn truncate_to_cursor_rows(&mut self) {
let cursor_absolute_row = self.rows_to_cursor();
// We want to include the line _with_ the cursor, so add one here.
let rows_to_include = cursor_absolute_row + 1;
self.raw.truncate_to(rows_to_include);
self.rows = self.rows.min(rows_to_include);
// Reposition the cursor on the visible screen. If the cursor was not at the bottom of the
// grid _and_ we had scrollback available, then truncating the lines below the cursor will
// effectively move the contents down the grid, so we need to update the cursor to match.
// Ultimately, the _absolute_ position of the cursor in the grid contents should not change
// by truncating the content after the cursor.
let cursor_position_shift = cursor_absolute_row.saturating_sub(self.rows_to_cursor());
self.cursor.point.row += cursor_position_shift;
// Reset the max cursor to be the value of the cursor, since the the max cursor isn't
// guaranteed to be in the grid anymore.
self.max_cursor_point = self.cursor.point;
}
/// Truncate columns in Grid and Storage to specified target # of columns.
fn truncate_columns(&mut self, col_to_truncate_to: usize) {
self.raw.truncate_columns(col_to_truncate_to);
self.columns = col_to_truncate_to;
}
/// Truncate all columns to the right of the cursor (including the column of the cursor, if the cursor is
/// not at the end of a line that needs to wrap for the next character).
pub(super) fn truncate_to_cursor_cols(&mut self) {
// If the cursor indicates that the input needs to wrap for the next character AND the cursor is at the
// end of the line, then, by definition, the entire line that the cursor is on, INCLUDING the column that
// the cursor is on, should be preserved. Hence, we don't need to truncate any columns.
// Note the cursor columns are zero-indexed hence the +1 below!
if !(self.cols_to_cursor() + 1 == self.columns() && self.cursor.input_needs_wrap) {
// If the cursor is NOT at the end of the line, then the cursor should be positioned at the next
// available cell to print output to. Hence, we truncate all columns from the cursor's column
// onwards (INCLUSIVE).
self.truncate_columns(self.cols_to_cursor());
}
}
#[inline]
pub fn cursor_cell(&mut self) -> &mut Cell {
let mut point = self.cursor_point();
if point.row >= self.total_rows() || point.col >= self.columns() {
log::error!(
"Error retrieving cursor cell, cursor point was outside the bounds of the grid: {point:?}"
);
point = Point {
row: self.total_rows().saturating_sub(1),
col: self.columns().saturating_sub(1),
};
}
&mut self[&point]
}
pub(super) fn cursor_point(&self) -> Point {
Point::new(
self.history_size() + self.cursor.point.row.0,
self.cursor.point.col,
)
}
pub fn get(&self, index: usize) -> Option<&Row> {
if index < self.total_rows() {
Some(&self.raw[index])
} else {
None
}
}
}
impl Dimensions for GridStorage {
#[inline]
fn total_rows(&self) -> usize {
self.raw.len()
}
#[inline]
fn visible_rows(&self) -> usize {
self.rows
}
#[inline]
fn columns(&self) -> usize {
self.columns
}
}
#[cfg(test)]
#[path = "grid_test.rs"]
mod tests;
@@ -0,0 +1,475 @@
//! Grid resize and reflow.
use std::cmp::{min, Ordering};
use std::mem;
use crate::terminal::model::cell::{Cell, Flags};
use crate::terminal::model::grid::grid_storage::{Dimensions, GridStorage};
use crate::terminal::model::grid::row::Row;
use crate::terminal::model::index::{VisiblePoint, VisibleRow};
impl GridStorage {
/// Resize the grid's width and/or height.
pub fn resize(&mut self, reflow: bool, lines: usize, cols: usize, finished: bool) {
if lines == 0 {
return;
}
// Use empty template cell for resetting cells due to resize.
let template = mem::take(&mut self.cursor.template);
match self.columns.cmp(&cols) {
Ordering::Less => self.grow_cols(reflow, cols),
Ordering::Greater => self.shrink_cols(reflow, cols),
Ordering::Equal => (),
}
// If the grid is finished, don't let the number of visible rows exceed
// the number of rows to the cursor (including the cursor row).
let lines = if finished {
lines.min(self.cursor.point.row.0 + 1)
} else {
lines
};
match self.rows.cmp(&lines) {
Ordering::Less => self.grow_lines(lines),
Ordering::Greater => self.shrink_lines(lines),
Ordering::Equal => (),
}
// Restore template cell.
self.cursor.template = template;
}
/// Add lines to the visible area.
///
/// Alacritty keeps the cursor at the bottom of the terminal as long as there
/// is scrollback available. Once scrollback is exhausted, new lines are
/// simply added to the bottom of the screen.
fn grow_lines(&mut self, new_line_count: usize) {
let lines_added = new_line_count - self.rows;
// Need to resize before updating buffer.
self.raw.grow_visible_lines(new_line_count);
self.rows = new_line_count;
let history_size = self.history_size();
let from_history = min(history_size, lines_added);
// Move existing lines up for every line that couldn't be pulled from history.
if from_history != lines_added {
let delta = lines_added - from_history;
self.scroll_up(&(VisibleRow(0)..VisibleRow(new_line_count)), delta);
}
// Move cursor down for every line pulled from history.
self.saved_cursor.point.row += from_history;
self.cursor.point.row += from_history;
self.max_cursor_point.row += from_history;
self.decrease_scroll_limit(lines_added);
}
/// Remove lines from the visible area.
///
/// The behavior in Terminal.app and iTerm.app is to keep the cursor at the
/// bottom of the screen. This is achieved by pushing history "out the top"
/// of the terminal window.
///
/// Alacritty takes the same approach.
pub(crate) fn shrink_lines(&mut self, target: usize) {
// Scroll up to keep content inside the window.
let required_scrolling = (self.cursor.point.row + 1).saturating_sub(target).0;
let last_row = VisibleRow(target - 1);
if required_scrolling > 0 {
self.scroll_up(&(VisibleRow(0)..VisibleRow(self.rows)), required_scrolling);
// Clamp cursors to the new viewport size.
self.cursor.point.row = min(self.cursor.point.row, last_row);
}
// Clamp saved cursor, since only primary cursor is scrolled into viewport.
self.saved_cursor.point.row = min(self.saved_cursor.point.row, last_row);
if self.max_cursor_point.row > last_row {
self.max_cursor_point.row = last_row;
}
self.raw.rotate((self.rows - target) as isize);
self.raw.shrink_visible_lines(target);
self.rows = target;
}
/// Grow number of columns in each row, reflowing if necessary.
fn grow_cols(&mut self, reflow: bool, columns: usize) {
// Check if a row needs to be wrapped.
let should_reflow = |row: &Row| -> bool {
let len = row.len();
reflow && len > 0 && len < columns && row[len - 1].flags().contains(Flags::WRAPLINE)
};
self.columns = columns;
let mut reversed: Vec<Row> = Vec::with_capacity(self.raw.len());
let mut cursor_line_delta = 0_usize;
// Remove the linewrap special case, by moving the cursor outside of the grid.
if self.cursor.input_needs_wrap && reflow {
self.cursor.input_needs_wrap = false;
self.cursor.point.col += 1;
}
let mut rows = self.raw.take_all();
let raw_total_rows_len = rows.len();
for (i, mut row) in rows.drain(..).enumerate().rev() {
// Check if reflowing should be performed.
let last_row = match reversed.last_mut() {
Some(last_row) if should_reflow(last_row) => last_row,
_ => {
reversed.push(row);
continue;
}
};
// Confirm that the last row is a wrapped row.
debug_assert!(
last_row[last_row.len() - 1]
.flags()
.contains(Flags::WRAPLINE),
"Trying to reflow a non-wrapped row"
);
// Remove wrap flag before appending additional cells.
if let Some(cell) = last_row.last_mut() {
cell.flags_mut().remove(Flags::WRAPLINE);
}
// Remove leading spacers when reflowing wide char to the previous line.
let mut last_len = last_row.len();
if last_len >= 1
&& last_row[last_len - 1]
.flags()
.contains(Flags::LEADING_WIDE_CHAR_SPACER)
{
last_row.shrink(last_len - 1);
last_len -= 1;
}
// Don't try to pull more cells from the next line than available.
let mut num_wrapped = columns - last_len;
let len = min(row.len(), num_wrapped);
// Insert leading spacer when there's not enough room for reflowing wide char.
let mut cells = if row[len - 1].flags().contains(Flags::WIDE_CHAR) {
num_wrapped -= 1;
let mut cells = row.front_split_off(len - 1);
let mut spacer = Cell::default();
spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER);
cells.push(spacer);
cells
} else {
row.front_split_off(len)
};
// Add removed cells to previous row and reflow content.
last_row.append(&mut cells);
// We do not want to consider rows containing the end of prompt to be "clear", even if they are "empty".
let row_is_clear = row.is_clear() && row.has_no_end_of_prompt_marker();
// First, reflow the max_cursor position, if necessary
let max_cursor_line = self.rows - self.max_cursor_point.row.0 - 1;
if i == max_cursor_line && reflow {
let target = self.max_cursor_point.wrapping_sub(columns, num_wrapped);
self.max_cursor_point.col = target.col;
} else if row_is_clear && i < max_cursor_line {
self.max_cursor_point.row += 1;
}
// Next, reflow the actual cursor position - This has an impact on how much history is
// pulled in, so it includes control flow not needed for max_cursor.
let cursor_buffer_line = self.rows - self.cursor.point.row.0 - 1;
if i == cursor_buffer_line && reflow {
let visible_point_origin = VisiblePoint {
row: VisibleRow(0),
col: 0,
};
let mut adjust_cursor = false;
// We want to adjust the cursor properly IF we need to pull a row from scrollback history.
// This applies when:
// 1. The cursor adjustment would incorrectly saturate at (0, 0) if no adjustment was made.
// 2. We have at least 1 row in scrollback (raw_total_rows_len - self.rows >= 1)
if self.resize_fix_ff_enabled
&& row_is_clear
&& self.cursor.point.wrapping_sub(columns, num_wrapped) == visible_point_origin
&& self.cursor.point.wrapping_sub(columns, num_wrapped + 1)
== visible_point_origin
&& raw_total_rows_len - self.rows >= 1
{
// Fixes the bug of incorrect cursor position reflow in situations where we are
// re-growing the terminal window after shrinking it. Specifically, this results in
// scrollback history lines, which we want to pull back into the "visible lines".
// We rotate the cursor down to ensure that the wrapping subtraction logic below with
// `num_wrapped` can be completed correctly, without saturating at (0, 0) (which previously
// occurred, if the cursor isn't adjusted). Ultimately, the user-facing impact resulted in an
// incorrect cursor position comparison leading to incorrect block heights, since Warp
// erronenously believed a command to be "empty" (when comparing the "end of prompt" cursor
// to the "end of the command").
// Note that last_row is a wrapped line in this case (see `should_reflow` and `debug_assert` above)!
self.cursor.point.row += 1;
adjust_cursor = true;
}
// Resize cursor's line and reflow the cursor if necessary.
let mut target = self.cursor.point.wrapping_sub(columns, num_wrapped);
// Clamp to the last column, if no content was reflown with the cursor.
if target.col == 0 && row_is_clear {
self.cursor.input_needs_wrap = true;
target = target.wrapping_sub(columns, 1);
}
self.cursor.point.col = target.col;
// Get required cursor line changes. Since `num_wrapped` is smaller than `cols`
// this will always be either `0` or `1`.
let line_delta = self.cursor.point.row - target.row;
if line_delta != 0 && row_is_clear {
if self.resize_fix_ff_enabled && adjust_cursor {
// We move the cursor up a line, if the current row is being entirely reflowed and removed.
self.cursor.point.row = self.cursor.point.row - line_delta;
}
continue;
}
cursor_line_delta += line_delta;
} else if row_is_clear {
// Rotate cursor down if content below them was pulled from history.
if i < cursor_buffer_line {
self.cursor.point.row += 1;
}
// Don't push line into the new buffer.
continue;
}
if let Some(cell) = last_row.last_mut() {
// Set wrap flag if next line still has cells.
cell.flags_mut().insert(Flags::WRAPLINE);
}
reversed.push(row);
}
// Make sure we have at least the viewport filled.
if reversed.len() < self.rows {
let delta = self.rows - reversed.len();
self.cursor.point.row = self.cursor.point.row.saturating_sub(delta);
self.max_cursor_point.row = self.max_cursor_point.row.saturating_sub(delta);
reversed.resize_with(self.rows, || Row::new(columns));
}
// Pull content down to put cursor in correct position, or move cursor up if there's no
// more lines to delete below the cursor.
if cursor_line_delta != 0 {
let cursor_buffer_line = self.rows - self.cursor.point.row.0 - 1;
let available = min(cursor_buffer_line, reversed.len() - self.rows);
let overflow = cursor_line_delta.saturating_sub(available);
reversed.truncate(reversed.len() + overflow - cursor_line_delta);
self.cursor.point.row = self.cursor.point.row.saturating_sub(overflow);
self.max_cursor_point.row = self.max_cursor_point.row.saturating_sub(overflow);
}
// Reverse iterator and fill all rows that are still too short.
let mut new_raw = Vec::with_capacity(reversed.len());
for mut row in reversed.drain(..).rev() {
if row.len() < columns {
row.grow(columns);
}
new_raw.push(row);
}
self.raw.replace_inner(new_raw);
// Confirm we haven't adjusted the cursor incorrectly into an invalid state (beyond max number of rows).
debug_assert!(
self.cursor.point.row <= VisibleRow(self.rows - 1),
"Cursor in invalid state (beyond max number of rows)"
);
}
/// Shrink number of columns in each row, reflowing if necessary.
fn shrink_cols(&mut self, reflow: bool, columns: usize) {
self.columns = columns;
// Remove the linewrap special case, by moving the cursor outside of the grid.
if self.cursor.input_needs_wrap && reflow {
self.cursor.input_needs_wrap = false;
self.cursor.point.col += 1;
}
let mut new_raw = Vec::with_capacity(self.raw.len());
let mut buffered: Option<Vec<Cell>> = None;
let mut rows = self.raw.take_all();
for (i, mut row) in rows.drain(..).enumerate().rev() {
// Append lines left over from the previous row.
if let Some(buffered) = buffered.take() {
// Add a column for every cell added before the cursor, if it goes beyond the new
// width it is then later reflown.
let cursor_buffer_line = self.rows - self.cursor.point.row.0 - 1;
if i == cursor_buffer_line {
self.cursor.point.col += buffered.len();
}
// Also add to the max_cursor position in the same manner
let max_cursor_line = self.rows - self.max_cursor_point.row.0 - 1;
if i == max_cursor_line {
self.max_cursor_point.col += buffered.len();
}
row.append_front(buffered);
}
loop {
// Remove all cells which require reflowing.
let mut wrapped = match row.shrink(columns) {
Some(wrapped) if reflow => wrapped,
_ => {
let cursor_buffer_line =
self.rows.saturating_sub(self.cursor.point.row.0 + 1);
let max_cursor_line =
self.rows.saturating_sub(self.max_cursor_point.row.0 + 1);
if reflow
&& ((i == cursor_buffer_line && self.cursor.point.col >= columns)
|| (i == max_cursor_line && self.max_cursor_point.col >= columns))
{
// If there are empty cells before the cursor or max_cursor, we assume
// it is explicit whitespace and need to wrap it like normal content.
Vec::new()
} else {
// Since it fits, just push the existing line without any reflow.
new_raw.push(row);
break;
}
}
};
// Insert spacer if a wide char would be wrapped into the last column.
if row.len() >= columns && row[columns - 1].flags().contains(Flags::WIDE_CHAR) {
let mut spacer = Cell::default();
spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER);
let wide_char = mem::replace(&mut row[columns - 1], spacer);
wrapped.insert(0, wide_char);
}
// Remove wide char spacer before shrinking.
let len = wrapped.len();
if len > 0
&& wrapped[len - 1]
.flags()
.contains(Flags::LEADING_WIDE_CHAR_SPACER)
{
if len == 1 {
row[columns - 1].flags_mut().insert(Flags::WRAPLINE);
new_raw.push(row);
break;
} else {
// Remove the leading spacer from the end of the wrapped row.
wrapped[len - 2].flags_mut().insert(Flags::WRAPLINE);
wrapped.truncate(len - 1);
}
}
new_raw.push(row);
// Set line as wrapped if cells got removed.
if let Some(cell) = new_raw.last_mut().and_then(|r| r.last_mut()) {
cell.flags_mut().insert(Flags::WRAPLINE);
}
if wrapped
.last()
.map(|c| c.flags().contains(Flags::WRAPLINE) && i >= 1)
.unwrap_or(false)
&& wrapped.len() < columns
{
// Make sure previous wrap flag doesn't linger around.
if let Some(cell) = wrapped.last_mut() {
cell.flags_mut().remove(Flags::WRAPLINE);
}
// Add removed cells to start of next row.
buffered = Some(wrapped);
break;
} else {
// Reflow cursor if a line below it is deleted.
let cursor_buffer_line = self.rows - self.cursor.point.row.0 - 1;
if (i == cursor_buffer_line && self.cursor.point.col < columns)
|| i < cursor_buffer_line
{
self.cursor.point.row = self.cursor.point.row.saturating_sub(1);
}
// Reflow the cursor if it is on this line beyond the width.
if i == cursor_buffer_line && self.cursor.point.col >= columns {
// Since only a single new line is created, we subtract only `cols`
// from the cursor instead of reflowing it completely.
self.cursor.point.col -= columns;
}
// Reflow max_cursor if a line below it is deleted
let max_cursor_line = self.rows - self.max_cursor_point.row.0 - 1;
if (i == max_cursor_line && self.max_cursor_point.col < columns)
|| i < max_cursor_line
{
self.max_cursor_point.row = self.max_cursor_point.row.saturating_sub(1);
}
// Reflow the max cursor if it is on this line beyond the width.
if i == max_cursor_line && self.max_cursor_point.col >= columns {
self.max_cursor_point.col -= columns;
}
// Make sure new row is at least as long as new width.
let occ = wrapped.len();
if occ < columns {
wrapped.resize_with(columns, Cell::default);
}
row = Row::from_vec(wrapped, occ);
}
}
}
// Reverse iterator and use it as the new grid storage.
let mut reversed: Vec<Row> = new_raw.drain(..).rev().collect();
reversed.truncate(self.max_scroll_limit + self.rows);
self.raw.replace_inner(reversed);
// Reflow the cursor and max_cursor positions, clamping if reflow is disabled
if !reflow {
self.cursor.point.col = min(self.cursor.point.col, columns - 1);
self.max_cursor_point.col = min(self.max_cursor_point.col, columns - 1);
} else {
if self.cursor.point.col == columns
&& !self[self.cursor_point().row][columns - 1]
.flags
.contains(Flags::WRAPLINE)
{
self.cursor.input_needs_wrap = true;
self.cursor.point.col -= 1;
} else {
self.cursor.point = self.cursor.point.wrap(columns);
}
self.max_cursor_point = self.max_cursor_point.wrap(columns);
}
// Clamp the saved cursor to the grid.
self.saved_cursor.point.col = min(self.saved_cursor.point.col, columns - 1);
}
}
+957
View File
@@ -0,0 +1,957 @@
use std::num::NonZeroUsize;
use crate::terminal::model::ansi::{self, Handler as _};
use crate::terminal::model::grid::grid_handler::GridHandler;
use crate::terminal::model::index::Point;
use crate::terminal::model::secrets::ObfuscateSecrets;
use crate::terminal::model::{
grid::Dimensions,
index::{VisiblePoint, VisibleRow},
};
use crate::terminal::SizeInfo;
use super::GridStorage;
macro_rules! assert_cell_char_eq {
($grid:ident[$row:literal][$col:literal], $expected:literal) => {
let row = $grid.row($row).expect("row should exist");
assert_eq!(row[$col].c, $expected);
};
}
#[test]
fn test_grid_scroll_down() {
// this simulates the alt-screen with 5 rows
let mut grid = GridStorage::new(5, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_down(&(VisibleRow(0)..VisibleRow(5)), 1);
assert_eq!(grid[0][0].c, '\0');
assert_eq!(grid[1][0].c, 'a');
assert_eq!(grid[2][0].c, 'b');
assert_eq!(grid[3][0].c, 'c');
assert_eq!(grid[4][0].c, 'd');
}
#[test]
fn test_grid_scroll_down_with_fixed_line() {
// this simulates the alt-screen with a fixed row at the top and bottom
let mut grid = GridStorage::new(5, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_down(&(VisibleRow(1)..VisibleRow(4)), 1);
assert_eq!(grid[0][0].c, 'a');
assert_eq!(grid[1][0].c, '\0');
assert_eq!(grid[2][0].c, 'b');
assert_eq!(grid[3][0].c, 'c');
assert_eq!(grid[4][0].c, 'e');
}
#[test]
fn test_grid_scroll_up() {
// this simulates the alt-screen with 5 rows
let mut grid = GridStorage::new(5, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_up(&(VisibleRow(0)..VisibleRow(5)), 1);
assert_eq!(grid[0][0].c, 'b');
assert_eq!(grid[1][0].c, 'c');
assert_eq!(grid[2][0].c, 'd');
assert_eq!(grid[3][0].c, 'e');
assert_eq!(grid[4][0].c, '\0');
assert_eq!(grid.num_lines_truncated, 1);
// this simulates the alt-screen with a fixed row at the top and bottom
let mut grid = GridStorage::new(5, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_up(&(VisibleRow(1)..VisibleRow(4)), 1);
assert_eq!(grid[0][0].c, 'a');
assert_eq!(grid[1][0].c, 'c');
assert_eq!(grid[2][0].c, 'd');
assert_eq!(grid[3][0].c, '\0');
assert_eq!(grid[4][0].c, 'e');
assert_eq!(grid.num_lines_truncated, 1);
}
#[test]
fn test_grid_scroll_up_by_multiple_lines() {
let mut grid = GridStorage::new(5, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_up(&(VisibleRow(0)..VisibleRow(5)), 2);
assert_eq!(grid[0][0].c, 'c');
assert_eq!(grid[1][0].c, 'd');
assert_eq!(grid[2][0].c, 'e');
assert_eq!(grid[3][0].c, '\0');
assert_eq!(grid[4][0].c, '\0');
// Every line scrolled immediately becomes truncated since there's no scrollback buffer.
assert_eq!(grid.num_lines_truncated, 2);
}
#[test]
fn test_grid_scroll_up_only_some_lines_truncated() {
let mut grid = GridStorage::new(5, 1, 1, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_up(&(VisibleRow(0)..VisibleRow(5)), 2);
assert_eq!(grid[0][0].c, 'b');
assert_eq!(grid[1][0].c, 'c');
assert_eq!(grid[2][0].c, 'd');
assert_eq!(grid[3][0].c, 'e');
assert_eq!(grid[4][0].c, '\0');
// Only one line ('b') should be truncated even though we scrolled up by 2
// since the scrollback buffer has space for one row.
assert_eq!(grid.history_size(), 1);
assert_eq!(grid.num_lines_truncated, 1);
}
#[test]
fn test_grid_scroll_up_with_fixed_lines() {
// this simulates the alt-screen with a fixed row at the top and bottom
let mut grid = GridStorage::new(5, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
grid.scroll_up(&(VisibleRow(1)..VisibleRow(4)), 1);
assert_eq!(grid[0][0].c, 'a');
assert_eq!(grid[1][0].c, 'c');
assert_eq!(grid[2][0].c, 'd');
assert_eq!(grid[3][0].c, '\0');
assert_eq!(grid[4][0].c, 'e');
}
#[test]
fn test_grid_git_diff_or_log() {
// Imagine this is a git log.
// First, simulate the shell populating the visible screen.
let mut grid = GridStorage::new(5, 1, 2, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
assert_eq!(grid.total_rows(), 5);
// Second, simulate the user scrolling for more rows twice.
grid.scroll_up(&(VisibleRow(0)..VisibleRow(5)), 2); // use the scrollback
assert_eq!(grid.total_rows(), 7);
// Now, the shell populating these two new rows.
grid[5][0].c = 'f';
grid[6][0].c = 'g';
assert_eq!(grid[VisibleRow(0)][0].c, 'c'); // grid index 2
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, 'f');
assert_eq!(grid[VisibleRow(4)][0].c, 'g'); // grid index 6
// Third, simulate the user scrolling back towards the top
grid.scroll_down(&(VisibleRow(0)..VisibleRow(5)), 1);
assert_eq!(grid[VisibleRow(0)][0].c, '\0'); // the shell is responsible for populating this
assert_eq!(grid[VisibleRow(1)][0].c, 'c');
assert_eq!(grid[VisibleRow(2)][0].c, 'd');
assert_eq!(grid[VisibleRow(3)][0].c, 'e');
assert_eq!(grid[VisibleRow(4)][0].c, 'f');
// The off-screen region should still be intact
assert_eq!(grid[0][0].c, 'a');
assert_eq!(grid[1][0].c, 'b');
}
#[test]
fn test_truncate_cursor_bottom_scrollback() {
// Create a filled grid with 2 lines of scrollback buffer
let mut grid = GridHandler::new_for_test_with_scroll_limit(5, 1, 2);
{
let grid = grid.grid_storage_mut();
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
}
// Push two rows into scrollback
grid.scroll_up(2);
assert_eq!(grid.history_size(), 2);
// Verify the grid
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Set the cursor to the bottom of the grid
grid.set_cursor_point(4, 0);
// Truncate everything after the cursor
grid.truncate_to_cursor_rows();
// Verify that the grid is unchanged
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Verify that the cursor is unchanged
assert_eq!(grid.grid_storage().cursor.point.row, VisibleRow(4));
}
#[test]
fn test_truncate_cursor_bottom_no_scrollback() {
// Create a filled grid with no scrollback buffer
let mut grid = GridHandler::new_for_test_with_scroll_limit(5, 1, 0);
{
let grid = grid.grid_storage_mut();
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
}
// Push two rows off the screen
grid.scroll_up(2);
assert_eq!(grid.history_size(), 0);
// Verify the grid
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Set the cursor to the bottom of the grid
grid.set_cursor_point(4, 0);
// Truncate everything after the cursor
grid.truncate_to_cursor_rows();
// Verify that the grid is unchanged
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Verify that the cursor is unchanged
assert_eq!(grid.grid_storage().cursor.point.row, VisibleRow(4));
}
#[test]
fn test_truncate_cursor_middle_scrollback() {
// Create a filled grid with 2 lines of scrollback buffer
let mut grid = GridHandler::new_for_test_with_scroll_limit(5, 1, 2);
{
let grid = grid.grid_storage_mut();
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
}
// Push two rows into scrollback
grid.scroll_up(2);
// Verify the grid
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Set the cursor to the middle of the grid
grid.set_cursor_point(2, 0);
// Truncate everything after the cursor
grid.truncate_to_cursor_rows();
// Verify that the grid has pulled content from scrollback
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'a');
assert_eq!(grid[VisibleRow(1)][0].c, 'b');
assert_eq!(grid[VisibleRow(2)][0].c, 'c');
assert_eq!(grid[VisibleRow(3)][0].c, 'd');
assert_eq!(grid[VisibleRow(4)][0].c, 'e');
}
// Verify that the cursor is now at the bottom
assert_eq!(grid.grid_storage().cursor.point.row, VisibleRow(4));
}
#[test]
fn test_truncate_cursor_middle_partial_scrollback() {
// Create a filled grid with one line of scrollback buffer
let mut grid = GridHandler::new_for_test_with_scroll_limit(5, 1, 1);
{
let grid = grid.grid_storage_mut();
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
}
// Push two rows off the screen; one will stay in scrollback because that is the limit
grid.scroll_up(2);
assert_eq!(grid.history_size(), 1);
// Verify the grid
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Set the cursor to the middle of the grid
grid.set_cursor_point(2, 0);
// Truncate everything after the cursor
grid.truncate_to_cursor_rows();
// Expected behavior is that two rows below the cursor get truncated, and
// one row gets pulled into the grid from scrollback. This leaves a total
// of 4 visible rows, and none in scrollback.
assert_eq!(grid.visible_rows(), 4);
assert_eq!(grid.history_size(), 0);
// Verify that the grid has pulled content from scrollback
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'b');
assert_eq!(grid[VisibleRow(1)][0].c, 'c');
assert_eq!(grid[VisibleRow(2)][0].c, 'd');
assert_eq!(grid[VisibleRow(3)][0].c, 'e');
}
// Verify that the cursor has shifted
assert_eq!(grid.grid_storage().cursor.point.row, VisibleRow(3));
}
#[test]
fn test_truncate_cursor_middle_no_scrollback() {
// Create a filled grid with no scrollback buffer
let mut grid = GridHandler::new_for_test_with_scroll_limit(5, 1, 0);
{
let grid = grid.grid_storage_mut();
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid[3][0].c = 'd';
grid[4][0].c = 'e';
}
// Push two rows off the screen
grid.scroll_up(2);
// Verify the grid
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
assert_eq!(grid[VisibleRow(3)][0].c, '\0');
assert_eq!(grid[VisibleRow(4)][0].c, '\0');
}
// Set the cursor to the middle of the grid
grid.set_cursor_point(2, 0);
// Truncate everything after the cursor
grid.truncate_to_cursor_rows();
assert_eq!(grid.visible_rows(), 3);
assert_eq!(grid.history_size(), 0);
// Verify that the grid is truncated
{
let grid = grid.grid_storage();
assert_eq!(grid[VisibleRow(0)][0].c, 'c');
assert_eq!(grid[VisibleRow(1)][0].c, 'd');
assert_eq!(grid[VisibleRow(2)][0].c, 'e');
}
// Verify that the cursor is unchanged
assert_eq!(grid.grid_storage().cursor.point.row, VisibleRow(2));
}
#[test]
fn test_end_prompt_point() {
let mut grid = GridHandler::new_for_test_with_scroll_limit(5, 6, 1);
assert_eq!(grid.prompt_end_point(), None);
grid.grid_storage_mut()[2][1].mark_end_of_prompt(false);
assert_eq!(grid.prompt_end_point(), Some(Point::new(2, 1)));
}
#[test]
fn test_grid_truncate_to_cursor_cols() {
let mut grid = GridStorage::new(3, 6, 1, ObfuscateSecrets::No);
// Before:
// 012345
// 0 ab
// 1 c
// 2
grid[0][0].c = 'a';
grid[0][1].c = 'b';
grid[0][2].c = ' ';
grid[1][2].c = 'c';
grid[1][3].c = ' ';
// Simulate that the cursor just finished printing the characters above.
grid.cursor.point = VisiblePoint {
row: VisibleRow(1),
// Cursor goes one character beyond the last printed character
// i.e. the next cell we can print.
col: 4,
};
assert_eq!(grid.columns, 6);
// We expect this truncate the grid to 4 columns.
grid.truncate_to_cursor_cols();
// After:
// 0123
// 0 ab
// 1 c
// 2
// Verify we've truncated the columns and cell content still exists.
assert_eq!(grid.columns, 4);
assert_eq!(grid[1][2].c, 'c');
assert_eq!(grid[1][3].c, ' ');
}
/// Test to ensure we DO NOT truncate "true content" in the case where the cursor is at the end of the line
/// and indicates the input needs to be wrapped for the next character.
#[test]
fn test_grid_truncate_to_cursor_cols_full_wrapped_line() {
let mut grid = GridStorage::new(3, 6, 1, ObfuscateSecrets::No);
// Before:
// 012345
// 0 ab
// 1 abcdef <- cursor at (1, 5) with input_needs_wrap = true
// 2
grid[0][0].c = 'a';
grid[0][1].c = 'b';
grid[0][2].c = ' ';
grid[1][0].c = 'a';
grid[1][1].c = 'b';
grid[1][2].c = 'c';
grid[1][3].c = 'd';
grid[1][4].c = 'e';
grid[1][5].c = 'f';
// Cursor at end of line, with input_needs_wrap = true, indicating the next
// character should be wrapped!
grid.cursor.point = VisiblePoint {
row: VisibleRow(1),
col: 5,
};
grid.cursor.input_needs_wrap = true;
assert_eq!(grid.columns, 6);
// We expect this DOES NOT truncate the grid any further, due to the cursor state above.
grid.truncate_to_cursor_cols();
// Verify we haven't truncated columns and cell content still exists.
assert_eq!(grid.columns, 6);
assert_eq!(grid[0][1].c, 'b');
assert_eq!(grid[1][5].c, 'f');
}
#[test]
fn test_split_grid_cursor_last_position() {
// 1. Setup: Create and initialize the grid.
let mut grid = GridHandler::new_for_test_with_scroll_limit(4, 6, 4);
grid.grid_storage_mut().populate_from_array(&[
&['a', 'a', '\0', '\0', '\0', '\0'],
&['b', 'b', '\0', '\0', '\0', '\0'],
&['c', 'c', 'c', 'c', '\0', '\0'],
&['d', 'd', 'd', 'd', 'd', '\0'],
]);
assert_eq!(grid.dirty_cells_range(), None);
// We update the cursor, to create a dirty_cells_range
grid.update_cursor(|cursor| {
cursor.point.row = VisibleRow(3);
cursor.point.col = 5;
});
// The cursor marks the cell _after_ the last modified cell, so the range
// should extend to the cell before the cursor.
assert_eq!(
grid.dirty_cells_range(),
Some(Point::new(0, 0)..=Point::new(3, 4))
);
// Grid splitting only ever occurs from the main thread, so pretend to
// finish byte processing before we start the split. This should clear
// out the dirty cells range.
grid.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert!(grid.dirty_cells_range().is_none());
// 2. Preconditions: Check initial state.
assert_eq!(grid.columns(), 6);
assert_eq!(grid.grid_storage()[0][0].c, 'a'); // Should be reversed due to bottom_row = 0
// 3. Action: Split the grid.
match grid.split(NonZeroUsize::new(1).expect("should not be zero")) {
(grid, Some(bottom_grid)) => {
// 4. Postconditions: Verify split results.
// Check row and visible counts
assert_eq!(grid.total_rows(), 1);
assert_eq!(grid.visible_rows(), 1);
assert_eq!(grid.grid_storage().raw.len(), 1);
assert_eq!(bottom_grid.total_rows(), 3);
assert_eq!(bottom_grid.visible_rows(), 3);
assert_eq!(bottom_grid.grid_storage().raw.len(), 3);
// Verify character content for grid1 and grid2
assert_eq!(grid.grid_storage()[0][0].c, 'a');
assert_eq!(grid.grid_storage()[0][1].c, 'a');
assert_eq!(bottom_grid.grid_storage()[0][0].c, 'b');
assert_eq!(bottom_grid.grid_storage()[0][1].c, 'b');
assert_eq!(bottom_grid.grid_storage()[1][0].c, 'c');
assert_eq!(bottom_grid.grid_storage()[2][0].c, 'd');
// Check cursor position
assert_eq!(
grid.grid_storage().cursor.point,
VisiblePoint {
row: VisibleRow(0),
col: 5
}
);
assert_eq!(
bottom_grid.grid_storage().cursor.point,
VisiblePoint {
row: VisibleRow(2),
col: 5
}
);
// We shouldn't ever split a grid while we're in the middle of
// processing data, so the dirty_cells_range should always be
// empty (i.e.: None).
assert!(grid.dirty_cells_range().is_none());
assert!(bottom_grid.dirty_cells_range().is_none());
}
_ => {
panic!("Received None from split (split row exceeded Grid!)");
}
}
}
#[test]
fn test_split_grid_cursor_in_grid1() {
// 1. Setup: Create and initialize the grid.
let mut grid = GridHandler::new_for_test_with_scroll_limit(4, 6, 4);
grid.grid_storage_mut().populate_from_array(&[
&['a', 'a', '\0', '\0', '\0', '\0'],
&['b', 'b', '\0', '\0', '\0', '\0'],
&['c', 'c', 'c', 'c', '\0', '\0'],
&['d', 'd', 'd', 'd', 'd', '\0'],
]);
// Place the cursor somewhere in the first half of the grid.
grid.set_cursor_point(1, 1); // Cursor at 'b'
grid.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// 2. Preconditions: Check initial state.
assert_eq!(
grid.grid_storage().cursor.point,
VisiblePoint {
row: VisibleRow(1),
col: 1
}
);
// 3. Action: Split the grid at the third row (index 2).
match grid.split(NonZeroUsize::new(2).expect("should not be zero")) {
(grid, Some(bottom_grid)) => {
let grid = grid.grid_storage();
let bottom_grid = bottom_grid.grid_storage();
// 4. Postconditions: Verify split results.
// Check that cursor's position in grid1 is as expected.
assert_eq!(
grid.cursor.point,
VisiblePoint {
row: VisibleRow(1),
col: 1
}
);
// Ensure grid2's cursor is at the default (0, 0) position.
assert_eq!(
bottom_grid.cursor.point,
VisiblePoint {
row: VisibleRow(0),
col: 0
}
);
// Additionally, verify the contents to ensure the split was successful.
assert_eq!(grid[0][0].c, 'a');
assert_eq!(grid[1][0].c, 'b');
assert_eq!(bottom_grid[0][0].c, 'c');
assert_eq!(bottom_grid[1][0].c, 'd');
}
_ => {
panic!("Received None from split (split row exceeded Grid!)");
}
}
}
#[test]
fn test_split_already_split_grid() {
// 1. Setup: Create and initialize the grid.
let mut grid = GridHandler::new_for_test_with_scroll_limit(4, 6, 4);
grid.grid_storage_mut().populate_from_array(&[
&['a', 'a', '\0', '\0', '\0', '\0'],
&['b', 'b', '\0', '\0', '\0', '\0'],
&['c', 'c', 'c', 'c', '\0', '\0'],
&['d', 'd', 'd', 'd', 'd', '\0'],
]);
match grid.split(NonZeroUsize::new(2).expect("should not be zero")) {
(grid, Some(bottom_grid)) => {
// Preconditions: Check initial state of split grid.
assert_eq!(grid.columns(), 6);
assert_eq!(grid.total_rows(), 2);
assert_eq!(bottom_grid.columns(), 6);
assert_eq!(bottom_grid.total_rows(), 2);
// Action: Split the grid again.
match bottom_grid.split(NonZeroUsize::new(1).expect("should not be zero")) {
(bottom_grid, Some(bottom_grid_2)) => {
let bottom_grid = bottom_grid.grid_storage();
let bottom_grid_2 = bottom_grid_2.grid_storage();
// Postconditions: Verify split results for second split.
assert_eq!(bottom_grid.total_rows(), 1);
assert_eq!(bottom_grid.raw.len(), 1);
assert_eq!(bottom_grid_2.total_rows(), 1);
assert_eq!(bottom_grid_2.raw.len(), 1);
// Verify character content for grid2a and grid2b
assert_eq!(bottom_grid[0][0].c, 'c');
assert_eq!(bottom_grid_2[0][0].c, 'd');
}
_ => {
panic!("Received None from split (split row exceeded Grid!)");
}
}
}
_ => {
panic!("Received None from split (split row exceeded Grid!)");
}
}
}
#[test]
fn test_split_grid_cursor_at_split_point() {
let mut grid = GridHandler::new_for_test_with_scroll_limit(4, 6, 4);
grid.grid_storage_mut().populate_from_array(&[
&['a', 'a', '\0', '\0', '\0', '\0'],
&['b', 'b', '\0', '\0', '\0', '\0'],
&['c', 'c', 'c', 'c', '\0', '\0'],
&['d', 'd', 'd', 'd', 'd', '\0'],
]);
grid.set_cursor_point(2, 3); // Cursor at 'c'
grid.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
match grid.split(NonZeroUsize::new(2).expect("should not be zero")) {
(grid, Some(bottom_grid)) => {
let grid = grid.grid_storage();
let bottom_grid = bottom_grid.grid_storage();
// Check cursor position in grid2 since the cursor is at the split point.
assert_eq!(
bottom_grid.cursor.point,
VisiblePoint {
row: VisibleRow(0),
col: 3
}
);
// Check auxiliary data
assert_eq!(grid.total_rows(), 2);
assert_eq!(grid.raw.len(), 2);
assert_eq!(bottom_grid.total_rows(), 2);
assert_eq!(bottom_grid.raw.len(), 2);
// Verify character content for grid2a and grid2b
assert_eq!(grid[0][0].c, 'a');
assert_eq!(grid[1][0].c, 'b');
assert_eq!(bottom_grid[0][0].c, 'c');
assert_eq!(bottom_grid[1][0].c, 'd');
}
_ => {
panic!("Received None from split (split row exceeded Grid!)");
}
}
}
#[test]
fn test_split_grid_exceed_rows() {
let mut grid = GridHandler::new_for_test_with_scroll_limit(4, 6, 4);
grid.grid_storage_mut().populate_from_array(&[
&['a', 'a', '\0', '\0', '\0', '\0'],
&['b', 'b', '\0', '\0', '\0', '\0'],
&['c', 'c', 'c', 'c', '\0', '\0'],
&['d', 'd', 'd', 'd', 'd', '\0'],
]);
// Attempt to split beyond len which is illegal.
let (_top_grid, bottom_grid) = grid.split(NonZeroUsize::new(5).expect("should not be zero"));
assert!(bottom_grid.is_none());
}
/// Regression test for (CORE-1950), checks whether the grid splitting operation correctly
/// splits visible rows (appropriately handling scrollback history).
#[test]
fn test_split_grid_scrollback_visible_rows() {
// Setup: Create and initialize the grid.
let mut grid = GridHandler::new_for_test_with_scroll_limit(30, 6, 7);
grid.grid_storage_mut().populate_from_array(&[
// Comments describe rows _after_ resizes below.
&['a', '0', '\0', '\0', '\0', '\0'], // history
&['a', '1', '\0', '\0', '\0', '\0'], // history
&['a', '2', '\0', '\0', '\0', '\0'], // history
&['a', '3', '\0', '\0', '\0', '\0'], // visible row 0
&['a', '4', '\0', '\0', '\0', '\0'], // visible row 1 <---- split above this row
&['a', '5', '\0', '\0', '\0', '\0'], // visible row 2
&['a', '6', '\0', '\0', '\0', '\0'], // visible row 3
]);
grid.set_cursor_point(6, 2);
grid.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// Force rows to go into scrollback with consecutive resize operations.
grid.resize(SizeInfo::new_without_font_metrics(18, 4));
// We force 4 visible rows (the bottom rows from grid above).
grid.resize(SizeInfo::new_without_font_metrics(4, 8));
// Confirm the grid is in the expected state after resizing.
assert_eq!(grid.visible_rows(), 4);
assert_eq!(grid.total_rows(), 7);
assert_eq!(grid.history_size(), 3);
// Split the grid at the row containing "a4", so "a4\na5\na6" goes into the
// bottom grid and the rest remains in the top grid.
match grid.split(NonZeroUsize::new(4).expect("should not be zero")) {
(grid, Some(bottom_grid)) => {
// Check that visible rows were split correctly.
assert_eq!(grid.total_rows(), 4);
assert_eq!(grid.visible_rows(), 4);
assert_eq!(grid.history_size(), 0);
assert_eq!(bottom_grid.total_rows(), 3);
assert_eq!(bottom_grid.visible_rows(), 3);
assert_eq!(bottom_grid.history_size(), 0);
// Verify character content for both grids.
assert_cell_char_eq!(grid[0][1], '0');
assert_cell_char_eq!(grid[1][1], '1');
assert_cell_char_eq!(grid[2][1], '2');
assert_cell_char_eq!(grid[3][1], '3');
assert_cell_char_eq!(bottom_grid[0][1], '4');
assert_cell_char_eq!(bottom_grid[1][1], '5');
assert_cell_char_eq!(bottom_grid[2][1], '6');
// Verify cursor positions.
assert_eq!(
grid.grid_storage().cursor.point,
VisiblePoint {
row: VisibleRow(3),
col: 7
}
);
assert_eq!(
bottom_grid.grid_storage().cursor.point,
VisiblePoint {
row: VisibleRow(2),
col: 2
}
);
}
_ => {
panic!("Received None from split (split row exceeded Grid!)");
}
}
}
/// Regression test for checking whether row indices which are out of bounds of the visible rows
/// (but in bounds for total rows) are handled correctly i.e. splits are not allowed.
#[test]
fn test_split_grid_scrollback_visible_rows_out_of_bounds() {
// Setup: Create and initialize the grid.
let mut grid = GridHandler::new_for_test_with_scroll_limit(30, 6, 7);
grid.grid_storage_mut().populate_from_array(&[
// Comments describe rows _after_ resizes below.
&['a', '0', '\0', '\0', '\0', '\0'], // history
&['a', '1', '\0', '\0', '\0', '\0'], // history
&['a', '2', '\0', '\0', '\0', '\0'], // history
&['a', '3', '\0', '\0', '\0', '\0'], // visible row 0
&['a', '4', '\0', '\0', '\0', '\0'], // visible row 1 <---- split above this row
&['a', '5', '\0', '\0', '\0', '\0'], // visible row 2
&['a', '6', '\0', '\0', '\0', '\0'], // visible row 3
]);
grid.set_cursor_point(6, 2);
grid.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// Force rows to go into scrollback with consecutive resize operations.
grid.resize(SizeInfo::new_without_font_metrics(18, 4));
// We force 4 visible rows (the bottom rows from grid above).
grid.resize(SizeInfo::new_without_font_metrics(4, 8));
// Confirm the grid is in the expected state after resizing.
assert_eq!(grid.visible_rows(), 4);
assert_eq!(grid.total_rows(), 7);
assert_eq!(grid.history_size(), 3);
// Purposely split at a row which is out of bounds, to confirm behavior.
let (_top_grid, bottom_grid) = grid.split(NonZeroUsize::new(7).expect("should not be zero"));
assert!(bottom_grid.is_none());
}
#[test]
fn test_scrolling_up_updates_dirty_cells_range() {
let mut grid = GridHandler::new_for_test_with_scroll_limit(3, 4, 2);
grid.input_at_cursor("abcd");
let pre_scroll_cursor_point = grid.cursor_point();
assert!(
grid.dirty_cells_range().is_none(),
"input_at_cursor should clear dirty_cells_range"
);
assert_eq!(pre_scroll_cursor_point, Point { row: 1, col: 0 });
grid.scroll_up(1);
// Increasing the scroll limit by 1 will update the cursor position.
assert_eq!(grid.cursor_point(), Point { row: 2, col: 0 });
// The dirty cells range should have been updated to include everything
// up to the new cursor point.
let dirty_cells_range = grid
.dirty_cells_range()
.expect("dirty_cells_range should be non-empty");
assert_eq!(dirty_cells_range.start(), &pre_scroll_cursor_point);
assert_eq!(
grid.cursor_point(),
dirty_cells_range.end().wrapping_add(grid.columns(), 1)
);
}
#[test]
pub fn test_cursor_cell() {
let mut grid = GridStorage::new(3, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
grid.cursor.point = VisiblePoint {
row: VisibleRow(2),
col: 0,
};
let cell = grid.cursor_cell();
assert_eq!(cell.c, 'c');
}
#[test]
pub fn test_cursor_cell_out_of_bounds() {
let mut grid = GridStorage::new(3, 1, 0, ObfuscateSecrets::No);
grid[0][0].c = 'a';
grid[1][0].c = 'b';
grid[2][0].c = 'c';
let total_rows = grid.total_rows();
let total_cols = grid.columns();
// Row is out of bounds.
grid.cursor.point = VisiblePoint {
row: VisibleRow(total_rows + 1),
col: 0,
};
let cell = grid.cursor_cell();
assert_eq!(cell.c, 'c');
// Column is out of bounds.
grid.cursor.point = VisiblePoint {
row: VisibleRow(0),
col: total_cols + 1,
};
let cell = grid.cursor_cell();
assert_eq!(cell.c, 'c');
}
+63
View File
@@ -0,0 +1,63 @@
use crate::terminal::model::image_map::ImagePlacementData;
use super::{AbsolutePoint, AbsoluteRectangle, GridHandler};
use warp_terminal::model::Point;
impl GridHandler {
pub fn get_image_ids_in_range(
&self,
displayed_start_row: usize,
displayed_end_row: usize,
) -> Vec<ImagePlacement> {
if self.has_displayed_output() {
return vec![];
}
if displayed_start_row > displayed_end_row {
return vec![];
}
self.images
.get_image_ids_by_rectangle(AbsoluteRectangle::from_range(
displayed_start_row,
displayed_end_row,
self,
))
.into_iter()
.filter_map(|absolute_image_placement| {
absolute_image_placement
.top_left
.to_point(self)
.map(|top_left| ImagePlacement {
image_id: absolute_image_placement.image_id,
placement_id: absolute_image_placement.placement_id,
z_index: absolute_image_placement.z_index,
top_left,
})
})
.collect()
}
pub(in crate::terminal::model) fn has_image_in_row(&self, displayed_row: usize) -> bool {
if self.has_displayed_output() {
return false;
}
let absolute_row = AbsolutePoint::from_point(Point::new(displayed_row, 0), self).row;
self.images.has_image_in_row(absolute_row)
}
pub fn get_image_placement_data(
&self,
image_id: u32,
placement_id: u32,
) -> Option<&ImagePlacementData> {
self.images.get_image_placement_data(image_id, placement_id)
}
}
pub struct ImagePlacement {
pub image_id: u32,
pub placement_id: u32,
pub z_index: i32,
pub top_left: Point,
}
+285
View File
@@ -0,0 +1,285 @@
use std::ops::{Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo};
use warp_terminal::model::grid::cell::Cell;
use warp_terminal::model::grid::row::Row;
use crate::terminal::model::{
grid::Dimensions as _,
index::{Point, VisiblePoint, VisibleRow},
};
use super::{grid_handler::GridHandler, GridStorage};
pub(in crate::terminal::model) trait ConvertToAbsolute {
type Output;
/// Transforms a value from the VisibleScreen coordinate space to the grid
/// coordinate space.
fn convert_to_absolute(&self, grid: &GridHandler) -> Self::Output;
}
impl ConvertToAbsolute for VisibleRow {
type Output = usize;
fn convert_to_absolute(&self, grid: &GridHandler) -> Self::Output {
grid.history_size() + self.0
}
}
impl ConvertToAbsolute for VisiblePoint {
type Output = Point;
fn convert_to_absolute(&self, grid: &GridHandler) -> Self::Output {
Point::new(self.row.convert_to_absolute(grid), self.col)
}
}
/// Index with buffer offset.
impl Index<usize> for GridStorage {
type Output = Row;
#[inline]
fn index(&self, index: usize) -> &Row {
&self.raw[index]
}
}
impl IndexMut<usize> for GridStorage {
#[inline]
fn index_mut(&mut self, index: usize) -> &mut Row {
&mut self.raw[index]
}
}
impl Index<&Point> for GridStorage {
type Output = Cell;
#[inline]
fn index(&self, point: &Point) -> &Cell {
&self[point.row][point.col]
}
}
impl IndexMut<&Point> for GridStorage {
#[inline]
fn index_mut(&mut self, point: &Point) -> &mut Cell {
&mut self[point.row][point.col]
}
}
impl Index<Point> for GridStorage {
type Output = Cell;
#[inline]
fn index(&self, point: Point) -> &Cell {
&self[point.row][point.col]
}
}
impl IndexMut<Point> for GridStorage {
#[inline]
fn index_mut(&mut self, point: Point) -> &mut Cell {
&mut self[point.row][point.col]
}
}
impl Index<VisibleRow> for GridStorage {
type Output = Row;
#[inline]
fn index(&self, index: VisibleRow) -> &Row {
&self.raw[index]
}
}
impl IndexMut<VisibleRow> for GridStorage {
#[inline]
fn index_mut(&mut self, index: VisibleRow) -> &mut Row {
&mut self.raw[index]
}
}
/// A subset of lines in the grid.
///
/// May be constructed using Grid::region(..).
pub struct Region<'a> {
start: VisibleRow,
end: VisibleRow,
grid: &'a GridStorage,
}
/// A mutable subset of lines in the grid.
///
/// May be constructed using Grid::region_mut(..).
pub struct RegionMut<'a> {
start: VisibleRow,
end: VisibleRow,
grid: &'a mut GridStorage,
}
impl RegionMut<'_> {
/// Call the provided function for every item in this region.
pub fn each<F: Fn(&mut Cell)>(self, func: F) {
for row in self {
for item in row {
func(item)
}
}
}
}
pub trait IndexRegion<I> {
/// Get an immutable region of Self.
fn region(&self, _: I) -> Region<'_>;
/// Get a mutable region of Self.
fn region_mut(&mut self, _: I) -> RegionMut<'_>;
}
impl IndexRegion<Range<VisibleRow>> for GridStorage {
fn region(&self, index: Range<VisibleRow>) -> Region<'_> {
assert!(index.start < VisibleRow(self.visible_rows()));
assert!(index.end <= VisibleRow(self.visible_rows()));
assert!(index.start <= index.end);
Region {
start: index.start,
end: index.end,
grid: self,
}
}
fn region_mut(&mut self, index: Range<VisibleRow>) -> RegionMut<'_> {
assert!(index.start < VisibleRow(self.visible_rows()));
assert!(index.end <= VisibleRow(self.visible_rows()));
assert!(index.start <= index.end);
RegionMut {
start: index.start,
end: index.end,
grid: self,
}
}
}
impl IndexRegion<RangeTo<VisibleRow>> for GridStorage {
fn region(&self, index: RangeTo<VisibleRow>) -> Region<'_> {
assert!(index.end <= VisibleRow(self.visible_rows()));
Region {
start: VisibleRow(0),
end: index.end,
grid: self,
}
}
fn region_mut(&mut self, index: RangeTo<VisibleRow>) -> RegionMut<'_> {
assert!(index.end <= VisibleRow(self.visible_rows()));
RegionMut {
start: VisibleRow(0),
end: index.end,
grid: self,
}
}
}
impl IndexRegion<RangeFrom<VisibleRow>> for GridStorage {
fn region(&self, index: RangeFrom<VisibleRow>) -> Region<'_> {
assert!(index.start < VisibleRow(self.visible_rows()));
Region {
start: index.start,
end: VisibleRow(self.visible_rows()),
grid: self,
}
}
fn region_mut(&mut self, index: RangeFrom<VisibleRow>) -> RegionMut<'_> {
assert!(index.start < VisibleRow(self.visible_rows()));
RegionMut {
start: index.start,
end: VisibleRow(self.visible_rows()),
grid: self,
}
}
}
impl IndexRegion<RangeFull> for GridStorage {
fn region(&self, _: RangeFull) -> Region<'_> {
Region {
start: VisibleRow(0),
end: VisibleRow(self.visible_rows()),
grid: self,
}
}
fn region_mut(&mut self, _: RangeFull) -> RegionMut<'_> {
RegionMut {
start: VisibleRow(0),
end: VisibleRow(self.visible_rows()),
grid: self,
}
}
}
pub struct RegionIter<'a> {
end: VisibleRow,
cur: VisibleRow,
grid: &'a GridStorage,
}
pub struct RegionIterMut<'a> {
end: VisibleRow,
cur: VisibleRow,
grid: &'a mut GridStorage,
}
impl<'a> IntoIterator for Region<'a> {
type IntoIter = RegionIter<'a>;
type Item = &'a Row;
fn into_iter(self) -> Self::IntoIter {
RegionIter {
end: self.end,
cur: self.start,
grid: self.grid,
}
}
}
impl<'a> IntoIterator for RegionMut<'a> {
type IntoIter = RegionIterMut<'a>;
type Item = &'a mut Row;
fn into_iter(self) -> Self::IntoIter {
RegionIterMut {
end: self.end,
cur: self.start,
grid: self.grid,
}
}
}
impl<'a> Iterator for RegionIter<'a> {
type Item = &'a Row;
fn next(&mut self) -> Option<Self::Item> {
if self.cur < self.end {
let index = self.cur;
self.cur += 1;
Some(&self.grid[index])
} else {
None
}
}
}
impl<'a> Iterator for RegionIterMut<'a> {
type Item = &'a mut Row;
fn next(&mut self) -> Option<Self::Item> {
if self.cur < self.end {
let index = self.cur;
self.cur += 1;
unsafe { Some(&mut *(&mut self.grid[index] as *mut _)) }
} else {
None
}
}
}
+31
View File
@@ -0,0 +1,31 @@
mod displayed_output;
pub mod grid_handler;
mod grid_storage;
mod indexing;
mod selection_cursor;
mod storage;
pub(super) mod grapheme_cursor;
#[cfg(test)]
mod tests;
pub use warp_terminal::model::grid::row;
pub use displayed_output::RespectDisplayedOutput;
pub use grid_storage::*;
pub(super) use indexing::ConvertToAbsolute;
pub use indexing::IndexRegion;
pub use selection_cursor::SelectionCursor;
enum CursorDirection {
Up,
Down,
Left,
Right,
}
enum CursorState {
Valid,
Exhausted(CursorDirection),
Invalid,
}
+357
View File
@@ -0,0 +1,357 @@
use string_offset::ByteOffset;
use warp_terminal::model::{
grid::{
cell::{self, LineLength as _},
Dimensions as _,
},
Point, VisiblePoint, VisibleRow,
};
use crate::terminal::{model::grid::Cursor, SizeInfo};
use super::GridHandler;
impl GridHandler {
/// Resize terminal to new dimensions.
pub fn resize(&mut self, size: SizeInfo) {
self.ansi_handler_state.cell_width = size.cell_width_px.as_f32() as usize;
self.ansi_handler_state.cell_height = size.cell_height_px.as_f32() as usize;
let old_cols = self.columns();
let old_rows = self.visible_rows();
let num_cols = size.columns();
let num_rows = size.rows();
if old_cols == num_cols && old_rows == num_rows {
log::debug!("Term::resize dimensions unchanged");
return;
}
if num_rows == 0 {
log::debug!("Ignoring resize down to zero visible lines");
return;
}
log::debug!("New num_cols is {num_cols} and num_lines is {num_rows}");
if old_cols != num_cols {
// Recreate tabs list.
self.ansi_handler_state.tabs.resize(num_cols);
}
// Resize the internal storage structures.
self.resize_storage(num_rows, num_cols);
// Reset scrolling region.
self.ansi_handler_state.scroll_region = VisibleRow(0)..VisibleRow(self.visible_rows());
// If the current grid has secrets, we now need to rescan the grid to refind any secrets.
if !self.secrets.is_empty() {
self.scan_for_secrets_after_resize();
}
// Re-apply the grid filter, if one exists.
self.refilter_lines();
}
pub(super) fn resize_storage(&mut self, num_rows: usize, num_cols: usize) {
use std::cmp::min;
// If this is the alt screen, we can skip reflowing the grid and simply
// adjust the size of rows.
if self.ansi_handler_state.is_alt_screen {
// We should never finish the alt screen grid.
debug_assert!(!self.finished);
// We can delegate to the old grid resizing logic, as there's no
// flat storage for the alt screen.
self.grid.resize(false, num_rows, num_cols, self.finished);
return;
}
// Store information about the initial cursor position in the grid.
let cursor = InitialCursorState::new(
self.grid.cursor.point,
self.grid.cursor.input_needs_wrap,
self,
);
let saved_cursor = InitialCursorState::new(
self.grid.saved_cursor.point,
self.grid.saved_cursor.input_needs_wrap,
self,
);
let max_cursor = InitialCursorState::new(self.grid.max_cursor_point, false, self);
// Push all rows from grid storage into flat storage. We make sure not
// to truncate rows that exceed the maximum scrollback size, as we only
// want to apply that limit after we've pulled rows back out into the
// grid.
for row_idx in 0..self.grid.total_rows() {
self.flat_storage
.push_rows_without_truncation([&self.grid[VisibleRow(row_idx)]]);
}
// Now that all data is in flat storage, convert the cursor state to
// reference a flat storage content offset.
let cursor = cursor.into_content_offset(self);
let saved_cursor = saved_cursor.into_content_offset(self);
let max_cursor = max_cursor.into_content_offset(self);
// Resize flat storage.
self.flat_storage.set_columns(num_cols);
// If the grid is finished, don't let the number of visible rows exceed
// the number of total rows (i.e.: if we can't pop a full num_rows
// from flat storage, limit visible_rows to the number of rows we
// _could_ pop).
let visible_rows = if self.finished {
num_rows.min(self.flat_storage.total_rows())
} else {
num_rows
};
// Convert back from a content offset to an actual cursor position.
let cursor = cursor.into_cursor_point(num_cols, self);
let saved_cursor = saved_cursor.into_cursor_point(num_cols, self);
let max_cursor = max_cursor.into_cursor_point(num_cols, self);
// If we're reducing the number of visible rows, we want to first drop
// rows after the cursor before we start pushing rows into scrollback.
//
// It's easiest to think about this in the context of a traditional
// terminal. If the window has a height of 10 rows but only 5 rows of
// content, those 5 rows will be at the top of the window. If the
// window is resized to be 8 rows tall, the bottom two rows of the grid
// will be truncated.
//
// Here, we eliminate those final rows by not pushing them back into
// grid storage after pulling them out of flat storage.
let rows_after_cursor = self
.flat_storage
.total_rows()
.saturating_sub(cursor.row() + 1);
let shrink_amount = self.visible_rows().saturating_sub(num_rows);
let rows_to_drop = shrink_amount.min(rows_after_cursor);
let rows_to_pop = visible_rows + rows_to_drop;
// Pop the rows from the bottom of flat storage, and drop some of them
// if necessary.
//
// Note: This may produce a Vec with len < num_rows.
let mut grid_rows = self.flat_storage.pop_rows(rows_to_pop);
grid_rows.truncate(grid_rows.len().saturating_sub(rows_to_drop));
// Set `GridStorage` contents to the given number of rows.
self.grid.set_stored_rows(grid_rows, visible_rows, num_cols);
// Set the new cursor positions.
let history_size = self.history_size();
cursor.update_cursor(&mut self.grid.cursor, history_size);
saved_cursor.update_cursor(&mut self.grid.saved_cursor, history_size);
self.grid.max_cursor_point = max_cursor.into_visible_point(self);
// Clamp cursors to the new visible region.
//
// TODO(vorporeal): This can lead to a `max_cursor_point` that has
// content after it. We should decide if this is something important
// to fix or not. (The behavior is inherited from grid storage resize
// logic.)
let last_row = VisibleRow(visible_rows - 1);
self.grid.cursor.point.row = min(self.grid.cursor.point.row, last_row);
self.grid.max_cursor_point.row = min(self.grid.max_cursor_point.row, last_row);
self.grid.saved_cursor.point.row = min(self.grid.saved_cursor.point.row, last_row);
// Finally, make sure we don't have too many rows in scrollback.
self.flat_storage.apply_max_rows();
}
}
#[derive(Debug)]
enum InitialCursorState {
/// Cursor is at some point in the grid.
AtPoint(Point),
/// Cursor is at the cell after the given point in the grid.
///
/// We set this in two situations:
/// 1. When the cursor has `input_needs_wrap = True`, and
/// 2. When the cursor is over an empty cell, we describe it as being after
/// the preceding cell. This allows us to set `input_needs_wrap`
/// properly when doing the final conversion back to a cursor.
AtCellAfterPoint(Point),
}
impl InitialCursorState {
fn new(mut cursor_point: VisiblePoint, input_needs_wrap: bool, grid: &mut GridHandler) -> Self {
// Start by clamping the cursor to the visible region of the grid, just
// in case some bug causes it to end up in an invalid place.
if cursor_point.row.0 >= grid.visible_rows() {
#[cfg(debug_assertions)]
log::error!(
"cursor should not be outside the bounds of the grid! \
cursor at ({}, {}) but grid has {} rows and {} columns",
cursor_point.row,
cursor_point.col,
grid.total_rows(),
grid.columns()
);
cursor_point.row.0 = grid.visible_rows() - 1;
}
if cursor_point.col >= grid.columns() {
#[cfg(debug_assertions)]
log::error!(
"cursor should not be outside the bounds of the grid! \
cursor at ({}, {}) but grid has {} rows and {} columns",
cursor_point.row,
cursor_point.col,
grid.total_rows(),
grid.columns()
);
cursor_point.col = grid.columns() - 1;
}
let history_size = grid.history_size();
let mut point = Point {
row: cursor_point.row.0 + history_size,
col: cursor_point.col,
};
let mut cell_after_point = false;
let row = &grid.grid[cursor_point.row];
let cell_follows_newline = |point: Point| -> bool {
// The cell cannot follow a newline unless it's the first cell in the row.
if point.col > 0 {
return false;
}
// If the cursor is at the first cell in the first row, treat it as if it follows a newline.
let Some(prev_row_idx) = point.row.checked_sub(1) else {
return true;
};
// The cell follows a newline if the previous row does not wrap.
!grid.row_wraps(prev_row_idx)
};
if input_needs_wrap {
// If the input needs wrapping, the target cell is the one
// after the current cursor point.
cell_after_point = true;
} else if row[point.col].c == cell::DEFAULT_CHAR
&& point.col >= row.line_length()
&& !cell_follows_newline(point)
{
// If the cursor is on an empty cell at the end of a row and could
// wrap back to the previous cell, track it relative to the
// previous cell. This allows us to set `Cursor.input_needs_wrap`
// instead of putting the cursor at the start of an
// otherwise-unneeded blank row.
cell_after_point = true;
point = point.wrapping_sub(grid.columns(), 1);
}
// Mark the location of the cursor within the grid, to ensure that
// the cell under the cursor exists post-resize.
if let Some(super::StorageRow::GridStorage(row_idx)) = grid.storage_row(point.row) {
grid.grid[row_idx][point.col]
.flags
.insert(cell::Flags::HAS_CURSOR);
}
if cell_after_point {
Self::AtCellAfterPoint(point)
} else {
Self::AtPoint(point)
}
}
fn into_content_offset(self, grid: &GridHandler) -> CursorContentOffset {
match self {
Self::AtPoint(point) => {
let content_offset = grid
.flat_storage
.content_offset_at_point(point)
.expect("should have a content offset for point");
CursorContentOffset::AtPoint(content_offset)
}
Self::AtCellAfterPoint(point) => {
let content_offset = grid
.flat_storage
.content_offset_at_point(point)
.expect("should have a content offset for point");
CursorContentOffset::AtCellAfterPoint(content_offset)
}
}
}
}
enum CursorContentOffset {
/// Cursor is at the location with the given byte offset in flat storage.
AtPoint(ByteOffset),
/// Cursor is at cell _after_ the location with the given byte offset in
/// flat storage. This helps ensure we properly handle `input_needs_wrap`
/// cases.
AtCellAfterPoint(ByteOffset),
}
impl CursorContentOffset {
fn into_cursor_point(self, new_cols: usize, grid: &GridHandler) -> FinalCursorState {
match self {
Self::AtPoint(byte_offset) => FinalCursorState::AtPoint(
grid.flat_storage
.content_offset_to_point(byte_offset)
.expect("content offset should be valid"),
),
Self::AtCellAfterPoint(byte_offset) => {
let mut point = grid
.flat_storage
.content_offset_to_point(byte_offset)
.expect("content offset should be valid");
// All data is in flat storage at the moment, so we need to
// explicitly ask it about row wrapping.
let input_needs_wrap =
point.col == new_cols - 1 && !grid.flat_storage.row_wraps(point.row);
if !input_needs_wrap {
point = point.wrapping_add(new_cols, 1);
}
FinalCursorState::AtCellAfterPoint {
point,
input_needs_wrap,
}
}
}
}
}
enum FinalCursorState {
AtPoint(Point),
AtCellAfterPoint {
point: Point,
input_needs_wrap: bool,
},
}
impl FinalCursorState {
fn update_cursor(self, cursor: &mut Cursor, history_size: usize) {
let (point, input_needs_wrap) = match self {
FinalCursorState::AtPoint(point) => (point, false),
FinalCursorState::AtCellAfterPoint {
point,
input_needs_wrap,
} => (point, input_needs_wrap),
};
cursor.point = point.to_visible_point(history_size);
cursor.input_needs_wrap = input_needs_wrap;
}
fn into_visible_point(self, grid: &GridHandler) -> VisiblePoint {
let (Self::AtPoint(point) | Self::AtCellAfterPoint { point, .. }) = self;
point.to_visible_point(grid.history_size())
}
fn row(&self) -> usize {
let (Self::AtPoint(point) | Self::AtCellAfterPoint { point, .. }) = self;
point.row
}
}
+307
View File
@@ -0,0 +1,307 @@
use std::{collections::HashSet, ops::RangeInclusive};
use itertools::Itertools as _;
use crate::ai::blocklist::block::secret_redaction::find_secrets_in_text_with_levels;
use crate::terminal::model::grid::{grapheme_cursor, Dimensions as _};
use crate::terminal::model::terminal_model::RangeInModel;
use crate::terminal::model::{
grid::RespectDisplayedOutput,
index::{Direction, Point},
secrets::{
IsObfuscated, ObfuscateSecrets, Secret, SecretAndHandle, SecretHandle, SecretLevel,
SECRETS_DFA,
},
};
use super::GridHandler;
impl GridHandler {
pub fn num_secrets_obfuscated(&self) -> usize {
self.secrets.len()
}
pub fn get_secret_obfuscation(&self) -> ObfuscateSecrets {
self.secret_obfuscation_mode
}
/// Returns a tuple of [`Secret`] and [`SecretHandle`] at the given point (assuming the point is
/// the displayed location and needs to be translated to the original location) or `None` if none is identified.
pub fn secret_at_displayed_point(&self, displayed_point: Point) -> Option<SecretAndHandle<'_>> {
self.secrets
.get_by_point(displayed_point, self, RespectDisplayedOutput::Yes)
}
/// Returns a tuple of [`Secret`] and [`SecretHandle`] at the given point (assuming
/// the point is the original location in the grid) or `None` if none is identified.
pub fn secret_at_original_point(&self, original_point: Point) -> Option<SecretAndHandle<'_>> {
self.secrets
.get_by_point(original_point, self, RespectDisplayedOutput::No)
}
/// Returns a [`Secret`] identified by [`SecretHandle`] or `None` if none is identified.
pub fn secret_by_handle(&self, secret_handle: SecretHandle) -> Option<&Secret> {
self.secrets.get_by_handle(&secret_handle)
}
/// Finds all secrets with matching plaintext and updates whether or not they are obfuscated.
fn mark_matching_secrets(&mut self, secret_handle: &SecretHandle, is_obfuscated: IsObfuscated) {
let Some(secret) = self.secrets.get_by_handle(secret_handle) else {
return;
};
let secret_plaintext = self.generate_secret_plaintext(secret.range());
let Some(matching_secret_handles) =
self.secrets_in_plaintext.get(secret_plaintext.as_str())
else {
return;
};
for secret_handle in matching_secret_handles.iter() {
if let Err(e) = self.secrets.set_is_obfuscated(secret_handle, is_obfuscated) {
log::warn!("Unable to obfuscate secret: {e:?}");
}
}
}
pub(in crate::terminal::model) fn obfuscate_secrets(
&mut self,
obfuscate_secrets: ObfuscateSecrets,
) {
self.secret_obfuscation_mode = obfuscate_secrets;
}
/// Marks the secret identified by [`SecretHandle`] as obfuscated. Returns an `Err` if no secret
/// is identified by [`SecretHandle`].
pub fn obfuscate_secret(&mut self, secret_handle: SecretHandle) -> anyhow::Result<()> {
self.secrets
.set_is_obfuscated(&secret_handle, IsObfuscated::Yes)
}
/// Marks the secret identified by [`SecretHandle`] as unobfuscated. Returns an `Err` if no
/// secret is identified by [`SecretHandle`].
pub fn unobfuscate_secret(&mut self, secret_handle: SecretHandle) -> anyhow::Result<()> {
self.secrets
.set_is_obfuscated(&secret_handle, IsObfuscated::No)?;
self.mark_matching_secrets(&secret_handle, IsObfuscated::No);
Ok(())
}
/// Marks the range of points identified by `range` as a secret within the grid.
pub(in crate::terminal::model::grid) fn mark_secret_range(
&mut self,
range: RangeInclusive<Point>,
is_obfuscated: IsObfuscated,
plaintext: String,
secret_level: SecretLevel,
) {
let handle = SecretHandle::next();
let range = *range.start()..=*range.end();
let secret = Secret::new(is_obfuscated, range.clone(), secret_level);
self.secrets.insert(handle, secret, self.columns());
self.secrets_in_plaintext
.entry(plaintext)
.or_default()
.insert(handle);
}
/// Clears any secrets currently stored in the grid.
pub(in crate::terminal::model) fn clear_secrets(&mut self) {
self.secrets.clear();
self.secrets_in_plaintext.clear();
self.set_all_bytes_scanned_for_secrets(true);
}
/// Clears the secrets that are encompassed in the given range. Returns a range of the minimum
/// and maximum point of all of the secrets that were removed.
fn clear_secrets_in_range(
&mut self,
range: RangeInclusive<Point>,
) -> Option<RangeInclusive<Point>> {
// Determine the set of secrets that need to be removed and eagerly collect them into a vec.
// We can't mutate the cells or the secrets map directly within the iterator due to lifetime
// issues.
let secrets_to_remove = self
.secrets
.iter()
.filter_map(|(secret_handle, secret)| {
let secret_range = secret.range();
// If the secret intersects with the dirty range at all, clear it.
let ranges_intersect =
range.start() <= secret_range.end() && range.end() >= secret_range.start();
ranges_intersect.then_some((*secret_handle, secret_range))
})
.collect_vec();
let (_, secret_range) = secrets_to_remove.first()?;
let mut start_point = *secret_range.start();
let mut end_point = *secret_range.end();
for (secret_handle, secret_range) in secrets_to_remove {
start_point = start_point.min(*secret_range.start());
end_point = end_point.max(*secret_range.end());
self.secrets.remove(secret_handle, self.columns());
}
Some(start_point..=end_point)
}
/// Scans the entire grid (not just the dirty cells range) for secrets.
pub fn scan_full_grid_for_secrets(&mut self) {
let start_point = Point::new(0, 0);
let end_point = Point::new(self.total_rows(), self.columns());
self.scan_range_for_secrets(start_point..=end_point);
self.set_all_bytes_scanned_for_secrets(true);
}
/// Scans the grid for any secrets in the cells that are currently marked as "dirty".
/// To scan for secrets, we take the beginning and end of the dirty cell range and expand this
/// range to its word boundaries. We then invalidate any prior secret that is contained within
/// this range, and then scan for secrets using regular expressions.
/// Returns the number of secret matches found.
fn scan_dirty_cells_for_secrets(&mut self) {
let Some((dirty_range_start, dirty_range_end)) =
self.dirty_cells_range().map(RangeInclusive::into_inner)
else {
return;
};
// Expand both the start and end points to word boundaries. The range of dirty cells is not
// guaranteed to be at a word boundary, in which case we would incorrectly omit secrets that
// start before the range or end after the range.
let start_point = self
.nonblank_word_bound_before_point(dirty_range_start)
.unwrap_or(dirty_range_start);
let end_point = self
.nonblank_word_bound_after_point(dirty_range_end)
.unwrap_or(dirty_range_end);
self.scan_range_for_secrets(start_point..=end_point);
}
/// Rescans the entire grid for secrets after a resize.
pub(super) fn scan_for_secrets_after_resize(&mut self) {
// Clear the entire secret range map since the underlying ranges stored within the map may
// be out of date after the resize.
self.secrets.clear_ranges_after_resize();
self.scan_range_for_secrets(
Point::new(0, 0)..=Point::new(self.total_rows() - 1, self.columns() - 1),
);
}
/// Scans the given `range` for secrets. Any prior secrets within the range are removed from the
/// grid. Any match is visually redacted in the grid.
fn scan_range_for_secrets(&mut self, range: RangeInclusive<Point>) {
let old_unobfuscated_secrets = &self
.secrets
.iter()
.filter_map(|(_, secret)| {
if !secret.is_obfuscated() {
Some(self.generate_secret_plaintext(secret.range()))
} else {
None
}
})
.collect::<HashSet<String>>();
// Clear any secrets that may are encompassed by the range that we are now searching --
// they may no longer be valid.
let cleared_secrets_range = self.clear_secrets_in_range(range.clone());
let mut start_point = *range.start();
let mut end_point = *range.end();
// Adjust the start and end points of where we are scanning if we cleared any secrets that
// started _before_ the range we are searching or ended _after_ the range we are searching.
// If we don't expand the boundaries of where we are searching, we may end up incorrectly
// removing a secret without refinding it.
if let Some(cleared_secrets_range) = cleared_secrets_range {
start_point = start_point.min(*cleared_secrets_range.start());
end_point = end_point.max(*cleared_secrets_range.end());
}
let matches = self
.regex_iter(
start_point,
end_point,
Direction::Right,
&SECRETS_DFA.read(),
)
.collect_vec();
for secret_match in matches {
// We mark a secret as unobfuscated if there is a new secret in the dirty cells range
// that has the same plaintext as a previous secret that was unobfuscated.
let plaintext = self.generate_secret_plaintext(secret_match.clone());
let old_secret_found = old_unobfuscated_secrets.contains(plaintext.as_str());
let is_obfuscated = if old_secret_found {
IsObfuscated::No
} else {
IsObfuscated::Yes
};
// Determine the secret level by re-scanning the plaintext
let secret_level = self.determine_secret_level(&plaintext);
self.mark_secret_range(secret_match, is_obfuscated, plaintext, secret_level);
}
}
fn generate_secret_plaintext(&self, range: RangeInclusive<Point>) -> String {
let mut text = String::new();
let mut cursor = self.grapheme_cursor_from(*range.start(), grapheme_cursor::Wrap::All);
while let Some(item) = cursor.current_item() {
if !((item.point().row < range.end().row)
|| (item.point().row <= range.end().row && item.point().col <= range.end().col))
{
break;
}
text.push(item.cell().c);
cursor.move_forward();
}
text
}
/// Determines the secret level by re-scanning the plaintext using the rich content detection
/// which includes secret level information
fn determine_secret_level(&self, plaintext: &str) -> SecretLevel {
let secrets_with_levels = find_secrets_in_text_with_levels(plaintext);
// Find the first match that corresponds to our plaintext
// In case of multiple matches, we return the highest priority level
secrets_with_levels
.into_iter()
.map(|(_, level)| level)
.max_by_key(|level| level.priority())
.unwrap_or(SecretLevel::User) // Default to User level if no matches found
}
fn set_all_bytes_scanned_for_secrets(&mut self, value: bool) {
self.all_bytes_scanned_for_secrets = value;
}
pub(in crate::terminal::model) fn all_bytes_scanned_for_secrets(&self) -> bool {
self.all_bytes_scanned_for_secrets
}
pub(super) fn maybe_scan_dirty_cells_for_secrets(&mut self) {
if self.secret_obfuscation_mode.should_redact_secret() {
self.scan_dirty_cells_for_secrets();
self.all_bytes_scanned_for_secrets &= true;
} else {
self.all_bytes_scanned_for_secrets = false;
}
}
}
#[cfg(test)]
#[path = "secrets_tests.rs"]
mod tests;
@@ -0,0 +1,326 @@
use regex::Regex;
use crate::terminal::{
event_listener::ChannelEventListener,
model::{
ansi::{self, Handler as _},
blockgrid::BlockGrid,
},
SizeInfo,
};
use super::*;
fn secret_ranges(grid_handler: &GridHandler) -> Vec<RangeInclusive<Point>> {
grid_handler
.secrets
.ranges()
.map(|(range, _)| range)
.collect_vec()
}
fn empty_blockgrid(
rows: usize,
columns: usize,
max_scroll_limit: usize,
secret_obfuscation_mode: ObfuscateSecrets,
) -> BlockGrid {
BlockGrid::new(
SizeInfo::new_without_font_metrics(rows, columns),
max_scroll_limit,
ChannelEventListener::new_for_test(),
secret_obfuscation_mode,
Default::default(),
)
}
#[test]
fn test_secret_redacted_after_byte_processing() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("ABCD").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(5, 10, 1, ObfuscateSecrets::Yes);
let grid_handler = blockgrid.grid_handler_mut();
// Nothing has been inserted into the grid, there should be no secrets.
assert!(grid_handler.secrets.is_empty());
grid_handler.input_at_cursor("ABCD");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
// Delete one character (by moving the cursor back and replacing the cell's content with a
// space).
grid_handler.update_cursor(|cursor| cursor.point = cursor.point.wrapping_sub(10, 1));
grid_handler.grid_storage_mut().cursor_cell().c = ' ';
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should no longer be any secrets ("ABC" does not match the regex).
assert!(grid_handler.secrets.is_empty());
assert!(secret_ranges(grid_handler).is_empty());
}
#[test]
fn test_secret_redacted_after_multiple_byte_processing() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("ABCD").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(5, 10, 1, ObfuscateSecrets::Yes);
let grid_handler = blockgrid.grid_handler_mut();
// Nothing has been inserted into the grid, there should be no secrets.
assert!(grid_handler.secrets.is_empty());
grid_handler.input_at_cursor("AB");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should no longer be any secrets ("AB" does not match the regex).
assert!(grid_handler.secrets.is_empty());
assert!(secret_ranges(grid_handler).is_empty());
// Insert "C" into the grid.
grid_handler.input_at_cursor("C");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There shouldn't be any secrets ("ABC" does not match the regex).
assert!(grid_handler.secrets.is_empty());
assert!(secret_ranges(grid_handler).is_empty());
// Insert "D" into the grid.
grid_handler.input_at_cursor("D");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
}
#[test]
fn test_secret_redaction_unobfuscated_secret_remains_after_byte_processing() -> anyhow::Result<()> {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("ABCD").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(5, 10, 1, ObfuscateSecrets::Yes);
let grid_handler = blockgrid.grid_handler_mut();
// Nothing has been inserted into the grid, there should be no secrets.
assert!(grid_handler.secrets.is_empty());
grid_handler.input_at_cursor("ABCD");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
let secret_handle = grid_handler
.secrets
.iter()
.map(|(handle, _)| *handle)
.next()
.expect("Should be at least one secret in the grid");
grid_handler.unobfuscate_secret(secret_handle)?;
for _ in 0..2 {
grid_handler.update_cursor(|cursor| cursor.point = cursor.point.wrapping_sub(10, 1));
grid_handler.grid_storage_mut().cursor_cell().c = ' ';
}
grid_handler.input_at_cursor("CD");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret still.
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
let secret = grid_handler
.secrets
.iter()
.map(|(_, secret)| secret.clone())
.next()
.expect("Should be at least one secret in the grid");
assert!(!secret.is_obfuscated());
Ok(())
}
#[test]
fn test_secret_redaction_secret_remains_after_resize() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("ABCD").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(2, 5, 2, ObfuscateSecrets::Yes);
let grid_handler = blockgrid.grid_handler_mut();
// Nothing has been inserted into the grid, there should be no secrets.
assert!(grid_handler.secrets.is_empty());
grid_handler.input_at_cursor("ABCDE");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
// Resize the grid, to only include 2 columns. This will cause the the last two cells in the
// secret to wrap to the next line.
let size_info = SizeInfo::new_without_font_metrics(2, 2);
grid_handler.resize(size_info);
assert_eq!(grid_handler.total_rows(), 4);
// The range should now be from (0,0)..=(1,1)
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(1, 1)]
);
}
#[test]
fn test_bytes_processed_for_secrets_after_turning_redaction() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("abcd").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(2, 4, 2, ObfuscateSecrets::Yes);
let grid_handler = blockgrid.grid_handler_mut();
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
assert!(grid_handler.all_bytes_scanned_for_secrets());
// Turn off secret obfuscation.
grid_handler.obfuscate_secrets(ObfuscateSecrets::No);
// There should still only be one secret and all bytes should not be processed.
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert_eq!(grid_handler.secrets.len(), 1);
assert!(!grid_handler.all_bytes_scanned_for_secrets());
}
#[test]
fn test_bytes_processed_for_secrets_after_turning_redaction_on() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("abcd").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(2, 4, 2, ObfuscateSecrets::No);
let grid_handler = blockgrid.grid_handler_mut();
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should no secrets.
assert_eq!(grid_handler.secrets.len(), 0);
assert!(!grid_handler.all_bytes_scanned_for_secrets());
// Turn secret obfuscation on.
grid_handler.obfuscate_secrets(ObfuscateSecrets::Yes);
// Insert another secret and assert that all bytes are not processed.
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert!(!grid_handler.all_bytes_scanned_for_secrets());
}
#[test]
fn test_bytes_processed_for_secrets_after_turning_redaction_off_then_on() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("abcd").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(3, 4, 2, ObfuscateSecrets::Yes);
let grid_handler = blockgrid.grid_handler_mut();
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 1);
assert_eq!(
secret_ranges(grid_handler),
vec![Point::new(0, 0)..=Point::new(0, 3)]
);
assert!(grid_handler.all_bytes_scanned_for_secrets());
// Turn secret obfuscation off.
grid_handler.obfuscate_secrets(ObfuscateSecrets::No);
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert_eq!(grid_handler.secrets.len(), 1);
assert!(!grid_handler.all_bytes_scanned_for_secrets());
// Turn secret obfuscation back on, and ensure that all bytes are still not processed.
grid_handler.obfuscate_secrets(ObfuscateSecrets::Yes);
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert!(!grid_handler.all_bytes_scanned_for_secrets());
}
#[test]
fn test_bytes_processed_for_secrets_after_turning_redaction_on_then_off() {
crate::terminal::model::secrets::set_user_and_enterprise_secret_regexes(
[&Regex::new("abcd").expect("Should be able to construct regex")],
std::iter::empty(), // No enterprise secrets
);
let mut blockgrid = empty_blockgrid(3, 4, 2, ObfuscateSecrets::No);
let grid_handler = blockgrid.grid_handler_mut();
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
// There should be one secret in the range (0,0)..(0,3).
assert_eq!(grid_handler.secrets.len(), 0);
assert!(!grid_handler.all_bytes_scanned_for_secrets());
// Turn secret obfuscation on.
grid_handler.obfuscate_secrets(ObfuscateSecrets::Yes);
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert!(!grid_handler.all_bytes_scanned_for_secrets());
// Turn secret obfuscation back off, and ensure that all bytes are still not processed.
grid_handler.obfuscate_secrets(ObfuscateSecrets::No);
grid_handler.input_at_cursor("abcd");
grid_handler.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert!(!grid_handler.all_bytes_scanned_for_secrets());
}
@@ -0,0 +1,165 @@
use warp_terminal::model::grid::CellType;
use crate::terminal::model::index::Point;
use super::{grid_handler::GridHandler, CursorDirection, CursorState, Dimensions as _};
/// A structure to help with movement of the cursor for keyboard-driven
/// text selection.
pub struct SelectionCursor<'g> {
/// Reference to the underlying grid.
grid: &'g GridHandler,
/// Current position of the cursor within the grid.
pos: Point,
/// The state of the cursor.
cursor_state: CursorState,
}
impl<'g> SelectionCursor<'g> {
pub fn new(grid: &'g GridHandler, pos: Point) -> Self {
let mut cursor = Self {
grid,
pos,
cursor_state: CursorState::Invalid,
};
if cursor.current_point_valid() {
cursor.cursor_state = CursorState::Valid;
}
cursor
}
/// Returns the cursor's current position, if it is valid.
pub fn position(&self) -> Option<Point> {
matches!(self.cursor_state, CursorState::Valid).then_some(self.pos)
}
/// Moves the cursor forward by a single grapheme.
pub fn move_forward(&mut self) {
match self.cursor_state {
CursorState::Valid if self.has_next() => {
self.increment_cursor();
// If the cursor is on top of a wide char, move it forward an
// extra cell.
if self.is_wide_char_or_spacer() {
self.increment_cursor();
}
}
CursorState::Valid => {
self.cursor_state = CursorState::Exhausted(CursorDirection::Right);
}
CursorState::Exhausted(CursorDirection::Left) => {
self.cursor_state = CursorState::Valid;
}
_ => (),
}
}
/// Moves the cursor backward by a single grapheme.
pub fn move_backward(&mut self) {
match self.cursor_state {
CursorState::Valid if self.has_prev() => {
self.decrement_cursor();
// If the cursor is on top of a wide char, move it backward an
// extra cell.
if self.is_wide_char_or_spacer() {
self.decrement_cursor();
}
}
CursorState::Valid => {
self.cursor_state = CursorState::Exhausted(CursorDirection::Left);
}
CursorState::Exhausted(CursorDirection::Right) => {
self.cursor_state = CursorState::Valid;
}
_ => (),
}
}
/// Moves the cursor up a row.
///
/// Unlike the horizontal movement functions, this is not grapheme-aware -
/// the cursor may end up on top of a wide char spacer cell.
pub fn move_up(&mut self) {
match self.cursor_state {
CursorState::Valid if self.pos.row > 0 => {
self.pos.row -= 1;
}
CursorState::Valid => {
self.cursor_state = CursorState::Exhausted(CursorDirection::Up);
}
CursorState::Exhausted(CursorDirection::Down) => {
self.cursor_state = CursorState::Valid;
}
_ => (),
}
}
/// Moves the cursor down a row.
///
/// Unlike the horizontal movement functions, this is not grapheme-aware -
/// the cursor may end up on top of a wide char spacer cell.
pub fn move_down(&mut self) {
match self.cursor_state {
CursorState::Valid if self.pos.row < self.grid.total_rows() - 1 => {
self.pos.row += 1;
}
CursorState::Valid => {
self.cursor_state = CursorState::Exhausted(CursorDirection::Down);
}
CursorState::Exhausted(CursorDirection::Up) => {
self.cursor_state = CursorState::Valid;
}
_ => (),
}
}
fn increment_cursor(&mut self) {
if self.pos.col == self.grid.columns() - 1 {
self.pos.row += 1;
self.pos.col = 0;
} else {
self.pos.col += 1
}
}
fn decrement_cursor(&mut self) {
if self.pos.col == 0 {
self.pos.row -= 1;
self.pos.col = self.grid.columns() - 1;
} else {
self.pos.col -= 1;
}
}
/// Returns whether the current cursor point is on top of a wide char
/// (either the first or second cell).
fn is_wide_char_or_spacer(&self) -> bool {
matches!(
self.grid.cell_type(self.pos),
Some(CellType::WideChar) | Some(CellType::WideCharSpacer)
)
}
/// Returns whether the current cursor point is valid (i.e.: is within the
/// bounds of the grid).
fn current_point_valid(&self) -> bool {
self.pos.row < self.grid.total_rows() && self.pos.col < self.grid.columns()
}
/// Returns whether the cursor would be valid if it were incremented.
fn has_next(&self) -> bool {
(self.pos.row != self.grid.total_rows() - 1 || self.pos.col != self.grid.columns() - 1)
&& self.current_point_valid()
}
/// Returns whether the cursor would be valid if it were decremented.
fn has_prev(&self) -> bool {
(self.pos.row != 0 || self.pos.col != 0) && self.current_point_valid()
}
}
#[cfg(test)]
#[path = "selection_cursor_tests.rs"]
mod tests;
@@ -0,0 +1,54 @@
use super::*;
#[test]
fn test_cursor() {
let grid = GridHandler::new_for_test(5, 5);
let mut cursor = SelectionCursor::new(&grid, Point::new(0, 0));
assert_eq!(cursor.position(), Some(Point::new(0, 0)));
// Test moving the cursor up above the top of the grid and then back down.
cursor.move_up();
assert_eq!(cursor.position(), None);
cursor.move_down();
assert_eq!(cursor.position(), Some(Point::new(0, 0)));
// Test moving the cursor backward from the first cell, then forward again.
cursor.move_backward();
assert_eq!(cursor.position(), None);
cursor.move_forward();
assert_eq!(cursor.position(), Some(Point::new(0, 0)));
cursor.move_forward();
assert_eq!(cursor.position(), Some(Point::new(0, 1)));
cursor.move_forward();
assert_eq!(cursor.position(), Some(Point::new(0, 2)));
cursor.move_forward();
assert_eq!(cursor.position(), Some(Point::new(0, 3)));
cursor.move_forward();
assert_eq!(cursor.position(), Some(Point::new(0, 4)));
// Test line-wrapping both forward and backward across a line boundary.
cursor.move_forward();
assert_eq!(cursor.position(), Some(Point::new(1, 0)));
cursor.move_backward();
assert_eq!(cursor.position(), Some(Point::new(0, 4)));
cursor = SelectionCursor::new(&grid, Point::new(4, 4));
assert_eq!(cursor.position(), Some(Point::new(4, 4)));
// Test moving the cursor down from the bottom of the grid, then back up.
cursor.move_down();
assert_eq!(cursor.position(), None);
cursor.move_up();
assert_eq!(cursor.position(), Some(Point::new(4, 4)));
// Test moving the cursor forward from the end of the grid, then backward again.
cursor.move_forward();
assert_eq!(cursor.position(), None);
cursor.move_backward();
assert_eq!(cursor.position(), Some(Point::new(4, 4)));
}
+443
View File
@@ -0,0 +1,443 @@
use serde::{Deserialize, Serialize};
use std::cmp::{max, PartialEq};
use std::mem;
use std::ops::{Index, IndexMut};
use crate::terminal::model::grid::row::Row;
use crate::terminal::model::index::VisibleRow;
/// A circular buffer for optimizing indexing and rotation. In other words, a performant implementation of
/// a grid API.
///
/// The data is stored in a vector of rows where top_row tracks the very first row of the terminal (think, top row
/// of the visible grid) and bottom_row is the last visible grid row. The direction of the data can either be
/// forwards or backwards, depending on the feature flag SequentialStorage.
///
/// Rezeroing is the process of restoring the topmost line back to raw storage index 0. This allows us then
/// to extend or reduce the capacity of the underlying vector. The field "len" tracks the number of active
/// rows in the grid, which is different from the currently allocated capacity of the vector (self.inner.len()).
/// A more detailed explanation: when the grid grows in length, it does so in chunks of MAX_CACHE_SIZE - that way,
/// we don't extend the vector every newline (which constitutes a scroll_up event) but rather every 1000 rows. Because of
/// this, the value storage.len (number of active rows) will often be less than the value storage.inner.len()
/// (the actual allocated capacity).
///
/// The [`Storage::rotate`] and [`Storage::rotate_down`] functions are fast modular additions on
/// the internal [`bottom_row`] field. As compared with [`slice::rotate_left`] which must rearrange items
/// in memory.
///
/// As a consequence, both [`Index`] and [`IndexMut`] are reimplemented for this type to account
/// for the zeroth element not always being at the start of the allocation.
///
/// Because certain [`Vec`] operations are no longer valid on this type, no [`Deref`]
/// implementation is provided. Anything from [`Vec`] that should be exposed must be done so
/// manually.
///
/// [`slice::rotate_left`]: https://doc.rust-lang.org/std/primitive.slice.html#method.rotate_left
/// [`Deref`]: std::ops::Deref
/// [`bottom_row`]: #structfield.bottom_row
///
/// --------------------
///
/// Imagine we want to print Shakespeare plays in alphabetical order. Let's assume sequential storage.
///
/// 1) The first write takes place at index 0.
/// ┌────────────────────────────────┐
/// 0: │All's Well That Ends Well │
/// 1: │ │
/// 2: │ │
/// 3: │ │
/// 4: │ │
/// 5: │ │
/// 6: │ │
/// 7: │ │
/// 8: │ │
/// 9: │ │ <-- bottom_row (this is the bottom row because the grid
/// └────────────────────────────────┘ was initialized with 10 active rows)
/// 2) The grid fills up at 10 rows.
/// ┌────────────────────────────────┐
/// 0: │All's Well That Ends Well (1602)│
/// 1: │Antony and Cleopatra (1606) │
/// 2: │As You Like It (1599) │
/// 3: │Comedy of Errors (1589) │
/// 4: │Coriolanus (1607) │
/// 5: │Cymbeline (1609) │
/// 6: │Hamlet (1600) │
/// 7: │Henry IV, Part I (1597) │
/// 8: │Henry IV, Part II (1597) │
/// 9: │Henry V (1598) │ <-- bottom_row
/// └────────────────────────────────┘
///
/// 3) Adding 3 more rows means the oldest 3 rows are overwritten.
/// ┌────────────────────────────────┐
/// 0: │Henry VI, Part I (1591) |
/// 1: │Henry VI, Part II (1590) │
/// 2: │Henry VI, Part III (1590) │ <-- bottom_row
/// 3: │Comedy of Errors (1589) │ <-- self.top_row()
/// 4: │Coriolanus (1607) │
/// 5: │Cymbeline (1609) │
/// 6: │Hamlet (1600) │
/// 7: │Henry IV, Part I (1597) │
/// 8: │Henry IV, Part II (1597) │
/// 9: │Henry V (1598) │
/// └────────────────────────────────┘
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(super) struct Storage {
inner: Vec<Row>,
/// The bottommost row of the visible grid. This is the furthest the grid extends and the maximum grid index.
/// In an unrotated buffer, bottom_row = len - 1.
bottom_row: usize,
/// Number of visible lines. This is important because it's the initial grid size.
visible_lines: usize,
/// Total number of lines currently active in the terminal
///
/// Shrinking this length allows reducing the number of lines in the scrollback buffer without
/// having to truncate the raw `inner` buffer.
/// As long as `len` is bigger than `inner`, it is also possible to grow the scrollback buffer
/// without any additional insertions.
len: usize,
/// Whether or not the storage mechanism is reversed or sequential.
#[serde(skip)]
is_sequential: bool,
/// Maximum number of buffered lines outside of the grid for performance optimization.
/// Every time we extend the size of the grid, we do so in chunks of this size.
#[serde(skip)]
max_cache_size: usize,
}
impl PartialEq for Storage {
fn eq(&self, other: &Self) -> bool {
// Both storage buffers need to be truncated and zeroed.
assert_eq!(self.bottom_row, 0);
assert_eq!(other.bottom_row, 0);
self.inner == other.inner && self.len == other.len
}
}
impl Storage {
#[inline]
pub fn with_capacity(visible_lines: usize, cols: usize, is_sequential: bool) -> Storage {
// Initialize visible lines; the scrollback buffer is initialized dynamically.
let mut inner = Vec::with_capacity(visible_lines);
inner.resize_with(visible_lines, || Row::new(cols));
Self::with_rows(inner, is_sequential, visible_lines)
}
/// Initialize Storage with given vector of rows.
#[inline]
pub fn with_rows(rows: Vec<Row>, is_sequential: bool, visible_lines: usize) -> Storage {
let len = rows.len();
debug_assert!(
visible_lines <= len,
"Size of the visible grid cannot be bigger than the actual size of the grid."
);
let inner = rows;
// We set this to 1 when using flat storage because there is no scrollback buffer
// that we'll need to grow.
let max_cache_size = 1;
Storage {
inner,
bottom_row: 0,
visible_lines,
len,
is_sequential,
max_cache_size,
}
}
pub fn is_sequential(&self) -> bool {
self.is_sequential
}
/// Increase the number of lines in the buffer.
#[inline]
pub fn grow_visible_lines(&mut self, next: usize) {
// Number of lines the buffer needs to grow.
let growage = next - self.visible_lines;
let cols = self[0].len();
self.initialize(growage, cols);
// Update visible lines.
self.visible_lines = next;
}
/// Decrease the number of lines in the buffer.
#[inline]
pub fn shrink_visible_lines(&mut self, next: usize) {
// Shrink the size without removing any lines.
let shrinkage = self.visible_lines - next;
self.shrink_lines(shrinkage);
// Update visible lines.
self.visible_lines = next;
}
/// Shrink the number of lines in the buffer.
#[inline]
pub fn shrink_lines(&mut self, shrinkage: usize) {
self.len -= shrinkage;
// Free memory.
if self.inner.len() > self.len + self.max_cache_size {
self.truncate_unused_rows();
}
}
/// Truncate the invisible elements from the raw buffer.
#[inline]
pub fn truncate_unused_rows(&mut self) {
self.rezero();
self.inner.truncate(self.len);
self.inner.shrink_to_fit();
}
/// Truncate columns in Storage to specified target # of columns.
#[inline]
pub fn truncate_columns(&mut self, col_to_truncate_to: usize) {
for row in &mut self.inner {
row.truncate(col_to_truncate_to);
}
}
/// Truncate the ring buffer so that it only contains the `target_len` number of rows. Rows are
/// filled from bottom to top (bottom_row), so elements are deleted from the beginning of the buffer.
#[inline]
pub fn truncate_to(&mut self, target_len: usize) {
if target_len < self.len() {
self.rezero();
self.inner.drain(0..self.len - target_len);
self.len = target_len;
self.visible_lines = self.visible_lines.min(target_len);
}
// Now that we've shortened the vector, remove unused rows, if any.
self.truncate_unused_rows();
}
pub fn push_from_scrollback(&mut self, mut rows: Vec<Row>) {
let num_rows = rows.len();
if !self.is_sequential {
rows.reverse();
}
// Append the rows to the internal storage.
self.rezero();
self.inner.splice(self.len.., rows);
// Increase the length to account for the additional rows we added.
self.len += num_rows;
}
/// Dynamically grow the storage buffer at runtime.
#[inline]
pub fn initialize(&mut self, additional_rows: usize, cols: usize) {
if self.len + additional_rows > self.inner.len() {
self.rezero();
let realloc_size = self.inner.len() + max(additional_rows, self.max_cache_size);
self.inner.resize_with(realloc_size, || Row::new(cols));
}
self.len += additional_rows;
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
/// Swap two visible lines. This function takes visible rows and converts them
/// to grid indices and then raw storage indices.
pub fn swap_lines(&mut self, a: VisibleRow, b: VisibleRow) {
let a = self.from_grid_index(self.to_grid_index(a));
let b = self.from_grid_index(self.to_grid_index(b));
self.inner.swap(a, b);
}
/// Swap implementation for Row.
///
/// Exploits the known size of Row to produce a slightly more efficient
/// swap than going through slice::swap.
///
/// The default implementation from swap generates 8 movups and 4 movaps
/// instructions. This implementation achieves the swap in only 8 movups
/// instructions.
///
/// This function accepts grid indices.
pub fn swap(&mut self, a: usize, b: usize) {
debug_assert_eq!(mem::size_of::<Row>(), mem::size_of::<usize>() * 4);
let a = self.from_grid_index(a);
let b = self.from_grid_index(b);
unsafe {
// Cast to a qword array to opt out of copy restrictions and avoid
// drop hazards. Byte array is no good here since for whatever
// reason LLVM won't optimized it.
let a_ptr = self.inner.as_mut_ptr().add(a) as *mut usize;
let b_ptr = self.inner.as_mut_ptr().add(b) as *mut usize;
// Copy 1 qword at a time.
//
// The optimizer unrolls this loop and vectorizes it.
let mut tmp: usize;
for i in 0..4 {
tmp = *a_ptr.offset(i);
*a_ptr.offset(i) = *b_ptr.offset(i);
*b_ptr.offset(i) = tmp;
}
}
}
#[inline]
pub fn rotate(&mut self, count: isize) {
debug_assert!(count.unsigned_abs() <= self.inner.len());
let len = self.inner.len();
self.bottom_row = (self.bottom_row as isize + count + len as isize) as usize % len;
}
/// Move the bottommost row to indicate a lesser row value.
#[inline]
pub fn retract(&mut self, mut count: isize) {
debug_assert!(count.unsigned_abs() <= self.inner.len());
if self.is_sequential {
count *= -1;
}
let len = self.inner.len();
self.bottom_row = (self.bottom_row as isize + count + len as isize) as usize % len;
}
/// Move the bottommost row to indicate a greater row value.
#[inline]
pub fn extend(&mut self, mut count: isize) {
debug_assert!(count.unsigned_abs() <= self.inner.len());
if !self.is_sequential {
count *= -1;
}
let len = self.inner.len();
self.bottom_row = (self.bottom_row as isize + count + len as isize) as usize % len;
}
/// Update the raw storage buffer.
#[inline]
pub fn replace_inner(&mut self, vec: Vec<Row>) {
self.len = vec.len();
self.inner = vec;
self.bottom_row = 0;
}
/// Remove all rows from storage.
#[inline]
pub fn take_all(&mut self) -> Vec<Row> {
self.truncate_unused_rows();
let mut buffer = Vec::new();
mem::swap(&mut buffer, &mut self.inner);
self.len = 0;
buffer
}
/// Rotate the ringbuffer to reset `self.bottom_row` back to index `0`.
#[inline]
fn rezero(&mut self) {
if self.bottom_row == 0 {
return;
}
self.inner.rotate_left(self.bottom_row);
self.bottom_row = 0;
}
/// Convert from grid index to raw storage index
#[inline]
#[allow(clippy::wrong_self_convention)]
pub fn from_grid_index(&self, mut requested: usize) -> usize {
debug_assert!(requested < self.len);
// If our storage is not sequential... reverse the index
if !self.is_sequential {
requested = self.len - requested - 1;
}
let zeroed = self.bottom_row + requested;
// Use if/else instead of remainder here to improve performance.
//
// Requires `zeroed` to be smaller than `self.inner.len() * 2`,
// but both `self.bottom_row` and `requested` are always smaller than `self.inner.len()`.
if zeroed >= self.inner.len() {
zeroed - self.inner.len()
} else {
zeroed
}
}
/// Convert from visible row to grid index
#[inline]
pub fn to_grid_index(&self, row: VisibleRow) -> usize {
self.len - self.visible_lines + row.0
}
pub fn estimated_heap_usage_bytes(&self) -> usize {
// We expect all rows to have the same internal capacity, so we can
// avoid summing up the heap usage from every row.
self.inner.capacity() * self.inner[0].estimated_memory_usage_bytes()
}
}
impl Index<usize> for Storage {
type Output = Row;
#[inline]
fn index(&self, index: usize) -> &Self::Output {
&self.inner[self.from_grid_index(index)]
}
}
impl IndexMut<usize> for Storage {
#[inline]
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
let index = self.from_grid_index(index); // borrowck
&mut self.inner[index]
}
}
impl Index<VisibleRow> for Storage {
type Output = Row;
#[inline]
fn index(&self, row: VisibleRow) -> &Self::Output {
&self[self.to_grid_index(row)]
}
}
impl IndexMut<VisibleRow> for Storage {
#[inline]
fn index_mut(&mut self, row: VisibleRow) -> &mut Self::Output {
let grid_index = self.to_grid_index(row);
&mut self[grid_index]
}
}
#[cfg(test)]
#[path = "storage_test.rs"]
mod tests;
+881
View File
@@ -0,0 +1,881 @@
use crate::terminal::model::cell::Cell;
use crate::terminal::model::grid::row::Row;
use crate::terminal::model::grid::storage::Storage;
// Use a large value for `MAX_CACHE_SIZE` for testing.
const MAX_CACHE_SIZE: usize = 1_000;
#[test]
fn with_capacity() {
let storage = Storage::with_capacity(3, 1, false);
assert_eq!(storage.inner.len(), 3);
assert_eq!(storage.len, 3);
assert_eq!(storage.bottom_row, 0);
assert_eq!(storage.visible_lines, 3);
}
#[test]
fn testing_grid_to_raw_storage_indexing() {
// Macro testing converstion of grid indices to raw-storage indices
macro_rules! assert_index_mapping {
($storage:ident, $(($grid_idx:literal, $expected_storage_idx:literal)),+) => {
$(
let actual = $storage.from_grid_index($grid_idx);
assert_eq!(actual, $expected_storage_idx, "Expected grid index {} to map to storage index {} but got {actual}", $grid_idx, $expected_storage_idx);
)+
}
}
let mut storage = Storage::with_capacity(10, 1, false);
assert_index_mapping!(
storage,
(0, 9),
(1, 8),
(2, 7),
(3, 6),
(4, 5),
(5, 4),
(6, 3),
(7, 2),
(8, 1),
(9, 0)
);
// Simulate a shift (going over grid limits)
// Changing bottom_row to 8 should mean the oldest row (0) is at raw index 7
storage.bottom_row = 8;
assert_index_mapping!(
storage,
(0, 7),
(1, 6),
(2, 5),
(3, 4),
(4, 3),
(5, 2),
(6, 1),
(7, 0),
(8, 9),
(9, 8)
);
// Simulate a size increase.
// This causes a rezero-ing and we're back to regular reversed order.
storage.initialize(5, 1);
assert_index_mapping!(
storage,
(0, 14),
(1, 13),
(2, 12),
(3, 11),
(4, 10),
(5, 9),
(6, 8),
(7, 7),
(8, 6),
(9, 5)
);
}
#[test]
fn testing_visible_row_to_grid_indexing() {
// visible rows are always the bottom (for grids)
// Here, we have 10 visible rows and an overall grid of 15 rows.
let mut storage = Storage::with_capacity(10, 1, false);
storage.initialize(5, 1);
assert_eq!(
storage.to_grid_index(crate::terminal::model::index::VisibleRow(9)),
14
);
assert_eq!(
storage.to_grid_index(crate::terminal::model::index::VisibleRow(0)),
5
);
}
#[test]
fn indexing() {
let mut storage = Storage::with_capacity(3, 1, false);
storage[0] = filled_row('0');
storage[1] = filled_row('1');
storage[2] = filled_row('2');
assert_eq!(storage[0], filled_row('0'));
assert_eq!(storage[1], filled_row('1'));
assert_eq!(storage[2], filled_row('2'));
storage.bottom_row = 1;
// now it's 2, 0, 1
// indexing into storage takes a grid index and does its own conversion to a raw index
assert_eq!(storage[0], filled_row('2'));
assert_eq!(storage[1], filled_row('0'));
assert_eq!(storage[2], filled_row('1'));
}
#[test]
#[should_panic]
fn indexing_above_inner_len() {
let storage = Storage::with_capacity(1, 1, false);
let _ = &storage[2];
}
#[test]
fn rotate() {
let mut storage = Storage::with_capacity(3, 1, false);
storage.rotate(2);
assert_eq!(storage.bottom_row, 2);
storage.shrink_lines(2);
assert_eq!(storage.len, 1);
assert_eq!(storage.inner.len(), 1);
assert_eq!(storage.bottom_row, 0);
}
/// Grow the buffer one line at the end of the buffer.
///
/// Before:
/// 0: 0 <- Zero
/// 1: 1
/// 2: -
/// After:
/// 0: 0 <- Zero
/// 1: 1
/// 2: -
/// 3: Cell::default
/// ...
/// MAX_CACHE_SIZE: 0
#[test]
fn grow_after_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('-')],
bottom_row: 0,
visible_lines: 3,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Grow buffer.
storage.grow_visible_lines(4);
// Make sure the result is correct.
let mut expected = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('-')],
bottom_row: 0,
visible_lines: 4,
len: 4,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
expected
.inner
.append(&mut vec![filled_row('\0'); MAX_CACHE_SIZE]);
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Grow the buffer one line at the start of the buffer.
///
/// Before:
/// 0: -
/// 1: 0 <- Zero
/// 2: 1
/// After:
/// 0: 0 <- Zero
/// 1: 1
/// 2: -
/// 3: Cell::default
/// ...
/// MAX_CACHE_SIZE: 0
#[test]
fn grow_before_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![filled_row('-'), filled_row('0'), filled_row('1')],
bottom_row: 1,
visible_lines: 3,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Grow buffer.
storage.grow_visible_lines(4);
// Make sure the result is correct.
let mut expected = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('-')],
bottom_row: 0,
visible_lines: 4,
len: 4,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
expected
.inner
.append(&mut vec![filled_row('\0'); MAX_CACHE_SIZE]);
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Shrink the buffer one line at the start of the buffer.
///
/// Before:
/// 0: 2
/// 1: 0 <- Zero
/// 2: 1
/// After:
/// 0: 2 <- Hidden
/// 0: 0 <- Zero
/// 1: 1
#[test]
fn shrink_before_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![filled_row('2'), filled_row('0'), filled_row('1')],
bottom_row: 1,
visible_lines: 3,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Shrink buffer.
storage.shrink_visible_lines(2);
// Make sure the result is correct.
let expected = Storage {
inner: vec![filled_row('2'), filled_row('0'), filled_row('1')],
bottom_row: 1,
visible_lines: 2,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Shrink the buffer one line at the end of the buffer.
///
/// Before:
/// 0: 0 <- Zero
/// 1: 1
/// 2: 2
/// After:
/// 0: 0 <- Zero
/// 1: 1
/// 2: 2 <- Hidden
#[test]
fn shrink_after_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('2')],
bottom_row: 0,
visible_lines: 3,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Shrink buffer.
storage.shrink_visible_lines(2);
// Make sure the result is correct.
let expected = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('2')],
bottom_row: 0,
visible_lines: 2,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Shrink the buffer at the start and end of the buffer.
///
/// Before:
/// 0: 4
/// 1: 5
/// 2: 0 <- Zero
/// 3: 1
/// 4: 2
/// 5: 3
/// After:
/// 0: 4 <- Hidden
/// 1: 5 <- Hidden
/// 2: 0 <- Zero
/// 3: 1
/// 4: 2 <- Hidden
/// 5: 3 <- Hidden
#[test]
fn shrink_before_and_after_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 6,
len: 6,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Shrink buffer.
storage.shrink_visible_lines(2);
// Make sure the result is correct.
let expected = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 2,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Check that truncating columns works as expected.
///
/// Before (e indicates empty):
/// 0: 1 1 1 e e e <- Zero
/// 1: 2 2 e e e e
/// 2: 3 3 3 3 e e
/// 3: 4 e e e e e
/// 4: 5 5 e e e e
/// After:
/// 0: 1 1 1 e <- Zero
/// 1: 2 2 e e
/// 2: 3 3 3 3
/// 3: 4 e e e
/// 4: 5 5 e e
#[test]
fn truncate_columns() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
partially_filled_row('1', 3, 6),
partially_filled_row('2', 2, 6),
partially_filled_row('3', 4, 6),
partially_filled_row('4', 1, 6),
partially_filled_row('5', 2, 6),
],
bottom_row: 0,
visible_lines: 5,
len: 5,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Truncate buffer (columns).
storage.truncate_columns(4);
// Make sure the result is correct.
let expected = Storage {
inner: vec![
partially_filled_row('1', 3, 4),
partially_filled_row('2', 2, 4),
partially_filled_row('3', 4, 4),
partially_filled_row('4', 1, 4),
partially_filled_row('5', 2, 4),
],
bottom_row: 0,
visible_lines: 5,
len: 5,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Check that truncating columns works as expected.
///
/// Before (e indicates empty):
/// 0: 1 1 1 e e e <- Zero
/// 1: 2 2 e e e e
/// 2: 3 3 3 3 e e
/// 3: 4 e e e e e
/// 4: 5 5 5 5 5 e <- Hidden
/// After:
/// 0: 1 1 1 e <- Zero
/// 1: 2 2 e e
/// 2: 3 3 3 3
/// 3: 4 e e e
/// 4: 5 5 5 5 <- Hidden (truncated as well)
#[test]
fn truncate_columns_ignore_hidden_rows() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
partially_filled_row('1', 3, 6),
partially_filled_row('2', 2, 6),
partially_filled_row('3', 4, 6),
partially_filled_row('4', 1, 6),
partially_filled_row('5', 5, 6),
],
bottom_row: 0,
visible_lines: 4,
len: 4,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Truncate buffer (columns).
storage.truncate_columns(4);
// Make sure the result is correct.
let expected = Storage {
inner: vec![
partially_filled_row('1', 3, 4),
partially_filled_row('2', 2, 4),
partially_filled_row('3', 4, 4),
partially_filled_row('4', 1, 4),
partially_filled_row('5', 4, 4),
],
bottom_row: 0,
visible_lines: 4,
len: 4,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Check that when truncating all hidden lines are removed from the raw buffer.
///
/// Before:
/// 0: 4 <- Hidden
/// 1: 5 <- Hidden
/// 2: 0 <- Zero
/// 3: 1
/// 4: 2 <- Hidden
/// 5: 3 <- Hidden
/// After:
/// 0: 0 <- Zero
/// 1: 1
#[test]
fn truncate_invisible_lines() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 1,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Truncate buffer.
storage.truncate_unused_rows();
// Make sure the result is correct.
let expected = Storage {
inner: vec![filled_row('0'), filled_row('1')],
bottom_row: 0,
visible_lines: 1,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Truncate buffer only at the beginning.
///
/// Before:
/// 0: 1
/// 1: 2 <- Hidden
/// 2: 0 <- Zero
/// After:
/// 0: 1
/// 0: 0 <- Zero
#[test]
fn truncate_invisible_lines_beginning() {
// Setup storage area.
let mut storage = Storage {
inner: vec![filled_row('1'), filled_row('2'), filled_row('0')],
bottom_row: 2,
visible_lines: 1,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Truncate buffer.
storage.truncate_unused_rows();
// Make sure the result is correct.
let expected = Storage {
inner: vec![filled_row('0'), filled_row('1')],
bottom_row: 0,
visible_lines: 1,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Before:
/// 0: 1 <- Zero
/// 1: 2
/// 2: 0
/// After:
/// 0: 0 <- Zero
#[test]
fn truncate_to_no_invisible_lines_unrotated_buffer() {
let mut storage = Storage {
inner: vec![filled_row('1'), filled_row('2'), filled_row('0')],
bottom_row: 0,
visible_lines: 3,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
storage.truncate_to(1);
let expected = Storage {
inner: vec![filled_row('0')],
bottom_row: 0,
visible_lines: 1,
len: 1,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Before:
/// 0: 1
/// 1: 2 <- Zero
/// 2: 0
/// After:
/// 0: 1 <- Zero
#[test]
fn truncate_to_no_invisible_lines_rotated_buffer() {
let mut storage = Storage {
inner: vec![filled_row('1'), filled_row('2'), filled_row('0')],
bottom_row: 1,
visible_lines: 3,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
storage.truncate_to(1);
let expected = Storage {
inner: vec![filled_row('1')],
bottom_row: 0,
visible_lines: 1,
len: 1,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// Before:
/// 0: 3 <- Hidden
/// 1: 4 <- Zero
/// 2: 0
/// 3: 1
/// 4: 2
/// After:
/// 0: 2 <- Zero
/// 1: 3
#[test]
fn truncate_to_with_invisible_lines_rotated_buffer() {
let mut storage = Storage {
inner: vec![
filled_row('3'),
filled_row('4'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
],
bottom_row: 1,
visible_lines: 2,
// The grid has 1 hidden line (`inner` has 5 lines).
len: 4,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
storage.truncate_to(2);
let expected = Storage {
inner: vec![filled_row('1'), filled_row('2')],
bottom_row: 0,
visible_lines: 2,
len: 2,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
#[test]
fn truncate_to_with_larger_target_len() {
let mut storage = Storage {
inner: vec![filled_row('1')],
bottom_row: 0,
visible_lines: 1,
len: 1,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
storage.truncate_to(2);
let expected = Storage {
inner: vec![filled_row('1')],
bottom_row: 0,
visible_lines: 1,
len: 1,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.visible_lines, expected.visible_lines);
assert_eq!(storage.inner, expected.inner);
assert_eq!(storage.bottom_row, expected.bottom_row);
assert_eq!(storage.len, expected.len);
}
/// First shrink the buffer and then grow it again.
///
/// Before:
/// 0: 4
/// 1: 5
/// 2: 0 <- Zero
/// 3: 1
/// 4: 2
/// 5: 3
/// After Shrinking:
/// 0: 4 <- Hidden
/// 1: 5 <- Hidden
/// 2: 0 <- Zero
/// 3: 1
/// 4: 2
/// 5: 3 <- Hidden
/// After Growing:
/// 0: 4
/// 1: 5
/// 2: -
/// 3: 0 <- Zero
/// 4: 1
/// 5: 2
/// 6: 3
#[test]
fn shrink_then_grow() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 0,
len: 6,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Shrink buffer.
storage.shrink_lines(3);
// Make sure the result after shrinking is correct.
let shrinking_expected = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 0,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.inner, shrinking_expected.inner);
assert_eq!(storage.bottom_row, shrinking_expected.bottom_row);
assert_eq!(storage.len, shrinking_expected.len);
// Grow buffer.
storage.initialize(1, 1);
// Make sure the previously freed elements are reused.
let growing_expected = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 0,
len: 4,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.inner, growing_expected.inner);
assert_eq!(storage.bottom_row, growing_expected.bottom_row);
assert_eq!(storage.len, growing_expected.len);
}
#[test]
fn initialize() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
bottom_row: 2,
visible_lines: 0,
len: 6,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
// Initialize additional lines.
let init_size = 3;
storage.initialize(init_size, 1);
// Generate expected grid.
let mut expected_inner = vec![
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
filled_row('4'),
filled_row('5'),
];
let expected_init_size = std::cmp::max(init_size, MAX_CACHE_SIZE);
expected_inner.append(&mut vec![filled_row('\0'); expected_init_size]);
let expected_storage = Storage {
inner: expected_inner,
bottom_row: 0,
visible_lines: 0,
len: 9,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
assert_eq!(storage.len, expected_storage.len);
assert_eq!(storage.bottom_row, expected_storage.bottom_row);
assert_eq!(storage.inner, expected_storage.inner);
}
#[test]
fn rotate_wrap_zero() {
let mut storage = Storage {
inner: vec![filled_row('-'), filled_row('-'), filled_row('-')],
bottom_row: 2,
visible_lines: 0,
len: 3,
is_sequential: false,
max_cache_size: MAX_CACHE_SIZE,
};
storage.rotate(2);
assert!(storage.bottom_row < storage.inner.len());
}
fn filled_row(content: char) -> Row {
let mut row = Row::new(1);
let mut cell = Cell::default();
cell.c = content;
row[0] = cell;
row
}
fn partially_filled_row(content: char, columns_to_fill: usize, total_columns: usize) -> Row {
let mut row = Row::new(total_columns);
for col in 0..total_columns {
let mut cell = Cell::default();
if col <= columns_to_fill {
cell.c = content;
}
row[col] = cell;
}
row
}
File diff suppressed because it is too large Load Diff