Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user