Use dynamic storage for zerowidth characters

The zerowidth characters were conventionally stored in a [char; 5].
This creates problems both by limiting the maximum number of zerowidth
characters and by increasing the cell size beyond what is necessary even
when no zerowidth characters are used.

Instead of storing zerowidth characters as a slice, a new CellExtra
struct is introduced which can store arbitrary optional cell data that
is rarely required. Since this is stored behind an optional pointer
(Option<Box<CellExtra>>), the initialization and dropping in the case
of no extra data are extremely cheap and the size penalty to cells
without this extra data is limited to 8 instead of 20 bytes.

The most noticible difference with this PR should be a reduction in
memory size of up to at least 30% (1.06G -> 733M, 100k scrollback, 72
lines, 280 columns). Since the zerowidth characters are now stored
dynamically, the limit of 5 per cell is also no longer present.
This commit is contained in:
Christian Duerr 2020-11-05 04:45:14 +00:00 committed by GitHub
parent 9028fb451a
commit ec42b42ce6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 625 additions and 594 deletions

View File

@ -36,6 +36,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Use yellow/red from the config for error and warning messages instead of fixed colors
- Existing CLI parameters are now passed to instances spawned using `SpawnNewInstance`
- Wayland's Client side decorations now use the search bar colors
- Reduce memory usage by up to at least 30% with a full scrollback buffer
- The number of zerowidth characters per cell is no longer limited to 5
### Fixed

View File

@ -473,10 +473,10 @@ impl Display {
// Iterate over all non-empty cells in the grid.
for cell in grid_cells {
// Update URL underlines.
urls.update(size_info.cols(), cell);
urls.update(size_info.cols(), &cell);
// Update underline/strikeout.
lines.update(cell);
lines.update(&cell);
// Draw the cell.
api.render_cell(cell, glyph_cache);

View File

@ -34,7 +34,6 @@ use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side};
use alacritty_terminal::selection::{Selection, SelectionType};
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::cell::Cell;
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
#[cfg(not(windows))]
use alacritty_terminal::tty;
@ -1174,7 +1173,7 @@ impl<N: Notify + OnResize> Processor<N> {
fn write_ref_test_results<T>(&self, terminal: &Term<T>) {
// Dump grid state.
let mut grid = terminal.grid().clone();
grid.initialize_all(Cell::default());
grid.initialize_all();
grid.truncate();
let serialized_grid = json::to_string(&grid).expect("serialize grid");

View File

@ -19,7 +19,7 @@ use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use alacritty_terminal::config::Cursor;
use alacritty_terminal::index::{Column, Line};
use alacritty_terminal::term::cell::{self, Flags};
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::{CursorKey, RenderableCell, RenderableCellContent, SizeInfo};
use alacritty_terminal::thread;
@ -436,7 +436,7 @@ impl Batch {
Self { tex: 0, instances: Vec::with_capacity(BATCH_MAX) }
}
pub fn add_item(&mut self, cell: RenderableCell, glyph: &Glyph) {
pub fn add_item(&mut self, cell: &RenderableCell, glyph: &Glyph) {
if self.is_empty() {
self.tex = glyph.tex_id;
}
@ -953,11 +953,7 @@ impl<'a> RenderApi<'a> {
.map(|(i, c)| RenderableCell {
line,
column: Column(i),
inner: RenderableCellContent::Chars({
let mut chars = [' '; cell::MAX_ZEROWIDTH_CHARS + 1];
chars[0] = c;
chars
}),
inner: RenderableCellContent::Chars((c, None)),
flags: Flags::empty(),
bg_alpha,
fg,
@ -971,7 +967,7 @@ impl<'a> RenderApi<'a> {
}
#[inline]
fn add_render_item(&mut self, cell: RenderableCell, glyph: &Glyph) {
fn add_render_item(&mut self, cell: &RenderableCell, glyph: &Glyph) {
// Flush batch if tex changing.
if !self.batch.is_empty() && self.batch.tex != glyph.tex_id {
self.render_batch();
@ -985,8 +981,8 @@ impl<'a> RenderApi<'a> {
}
}
pub fn render_cell(&mut self, cell: RenderableCell, glyph_cache: &mut GlyphCache) {
let chars = match cell.inner {
pub fn render_cell(&mut self, mut cell: RenderableCell, glyph_cache: &mut GlyphCache) {
let (mut c, zerowidth) = match cell.inner {
RenderableCellContent::Cursor(cursor_key) => {
// Raw cell pixel buffers like cursors don't need to go through font lookup.
let metrics = glyph_cache.metrics;
@ -1000,10 +996,10 @@ impl<'a> RenderApi<'a> {
self.cursor_config.thickness(),
))
});
self.add_render_item(cell, glyph);
self.add_render_item(&cell, glyph);
return;
},
RenderableCellContent::Chars(chars) => chars,
RenderableCellContent::Chars((c, ref mut zerowidth)) => (c, zerowidth.take()),
};
// Get font key for cell.
@ -1014,37 +1010,33 @@ impl<'a> RenderApi<'a> {
_ => glyph_cache.font_key,
};
// Don't render text of HIDDEN cells.
let mut chars = if cell.flags.contains(Flags::HIDDEN) {
[' '; cell::MAX_ZEROWIDTH_CHARS + 1]
} else {
chars
};
// Render tabs as spaces in case the font doesn't support it.
if chars[0] == '\t' {
chars[0] = ' ';
// Ignore hidden cells and render tabs as spaces to prevent font issues.
let hidden = cell.flags.contains(Flags::HIDDEN);
if c == '\t' || hidden {
c = ' ';
}
let mut glyph_key = GlyphKey { font_key, size: glyph_cache.font_size, c: chars[0] };
let mut glyph_key = GlyphKey { font_key, size: glyph_cache.font_size, c };
// Add cell to batch.
let glyph = glyph_cache.get(glyph_key, self);
self.add_render_item(cell, glyph);
self.add_render_item(&cell, glyph);
// Render zero-width characters.
for c in (&chars[1..]).iter().filter(|c| **c != ' ') {
glyph_key.c = *c;
let mut glyph = *glyph_cache.get(glyph_key, self);
// Render visible zero-width characters.
if let Some(zerowidth) = zerowidth.filter(|_| !hidden) {
for c in zerowidth {
glyph_key.c = c;
let mut glyph = *glyph_cache.get(glyph_key, self);
// The metrics of zero-width characters are based on rendering
// the character after the current cell, with the anchor at the
// right side of the preceding character. Since we render the
// zero-width characters inside the preceding character, the
// anchor has been moved to the right by one cell.
glyph.left += glyph_cache.metrics.average_advance as i16;
// The metrics of zero-width characters are based on rendering
// the character after the current cell, with the anchor at the
// right side of the preceding character. Since we render the
// zero-width characters inside the preceding character, the
// anchor has been moved to the right by one cell.
glyph.left += glyph_cache.metrics.average_advance as i16;
self.add_render_item(cell, &glyph);
self.add_render_item(&cell, &glyph);
}
}
}
}

View File

@ -150,14 +150,14 @@ impl RenderLines {
/// Update the stored lines with the next cell info.
#[inline]
pub fn update(&mut self, cell: RenderableCell) {
self.update_flag(cell, Flags::UNDERLINE);
self.update_flag(cell, Flags::DOUBLE_UNDERLINE);
self.update_flag(cell, Flags::STRIKEOUT);
pub fn update(&mut self, cell: &RenderableCell) {
self.update_flag(&cell, Flags::UNDERLINE);
self.update_flag(&cell, Flags::DOUBLE_UNDERLINE);
self.update_flag(&cell, Flags::STRIKEOUT);
}
/// Update the lines for a specific flag.
fn update_flag(&mut self, cell: RenderableCell, flag: Flags) {
fn update_flag(&mut self, cell: &RenderableCell, flag: Flags) {
if !cell.flags.contains(flag) {
return;
}

View File

@ -48,7 +48,7 @@ impl Url {
pub struct Urls {
locator: UrlLocator,
urls: Vec<Url>,
scheme_buffer: Vec<RenderableCell>,
scheme_buffer: Vec<(Point, Rgb)>,
last_point: Option<Point>,
state: UrlLocation,
}
@ -71,10 +71,10 @@ impl Urls {
}
// Update tracked URLs.
pub fn update(&mut self, num_cols: Column, cell: RenderableCell) {
pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) {
// Convert cell to character.
let c = match cell.inner {
RenderableCellContent::Chars(chars) => chars[0],
let c = match &cell.inner {
RenderableCellContent::Chars((c, _zerowidth)) => *c,
RenderableCellContent::Cursor(_) => return,
};
@ -109,9 +109,8 @@ impl Urls {
self.urls.push(Url { lines: Vec::new(), end_offset, num_cols });
// Push schemes into URL.
for scheme_cell in self.scheme_buffer.split_off(0) {
let point = scheme_cell.into();
self.extend_url(point, point, scheme_cell.fg, end_offset);
for (scheme_point, scheme_fg) in self.scheme_buffer.split_off(0) {
self.extend_url(scheme_point, scheme_point, scheme_fg, end_offset);
}
// Push the new cell into URL.
@ -120,7 +119,7 @@ impl Urls {
(UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => {
self.extend_url(point, end, cell.fg, end_offset);
},
(UrlLocation::Scheme, _) => self.scheme_buffer.push(cell),
(UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.into(), cell.fg)),
(UrlLocation::Reset, _) => self.reset(),
_ => (),
}
@ -196,13 +195,12 @@ mod tests {
use super::*;
use alacritty_terminal::index::{Column, Line};
use alacritty_terminal::term::cell::MAX_ZEROWIDTH_CHARS;
fn text_to_cells(text: &str) -> Vec<RenderableCell> {
text.chars()
.enumerate()
.map(|(i, c)| RenderableCell {
inner: RenderableCellContent::Chars([c; MAX_ZEROWIDTH_CHARS + 1]),
inner: RenderableCellContent::Chars((c, None)),
line: Line(0),
column: Column(i),
fg: Default::default(),
@ -223,7 +221,7 @@ mod tests {
let mut urls = Urls::new();
for cell in input {
urls.update(Column(num_cols), cell);
urls.update(Column(num_cols), &cell);
}
let url = urls.urls.first().unwrap();
@ -239,7 +237,7 @@ mod tests {
let mut urls = Urls::new();
for cell in input {
urls.update(Column(num_cols), cell);
urls.update(Column(num_cols), &cell);
}
assert_eq!(urls.urls.len(), 3);

View File

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::ansi::{CharsetIndex, StandardCharset};
use crate::index::{Column, IndexRange, Line, Point};
use crate::term::cell::{Cell, Flags};
use crate::term::cell::{Flags, ResetDiscriminant};
pub mod resize;
mod row;
@ -49,25 +49,24 @@ impl<T: PartialEq> ::std::cmp::PartialEq for Grid<T> {
}
}
pub trait GridCell {
pub trait GridCell: Sized {
/// Check if the cell contains any content.
fn is_empty(&self) -> bool;
/// Perform an opinionated cell reset based on a template cell.
fn reset(&mut self, template: &Self);
fn flags(&self) -> &Flags;
fn flags_mut(&mut self) -> &mut Flags;
/// Fast equality approximation.
///
/// This is a faster alternative to [`PartialEq`],
/// but might report unequal cells as equal.
fn fast_eq(&self, other: Self) -> bool;
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct Cursor {
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Cursor<T> {
/// The location of this cursor.
pub point: Point,
/// Template cell when using this cursor.
pub template: Cell,
pub template: T,
/// Currently configured graphic character sets.
pub charsets: Charsets,
@ -131,11 +130,11 @@ impl IndexMut<CharsetIndex> for Charsets {
pub struct Grid<T> {
/// Current cursor for writing data.
#[serde(skip)]
pub cursor: Cursor,
pub cursor: Cursor<T>,
/// Last saved cursor.
#[serde(skip)]
pub saved_cursor: Cursor,
pub saved_cursor: Cursor<T>,
/// Lines in the grid. Each row holds a list of cells corresponding to the
/// columns in that row.
@ -167,10 +166,10 @@ pub enum Scroll {
Bottom,
}
impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
pub fn new(lines: Line, cols: Column, max_scroll_limit: usize, template: T) -> Grid<T> {
impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
pub fn new(lines: Line, cols: Column, max_scroll_limit: usize) -> Grid<T> {
Grid {
raw: Storage::with_capacity(lines, Row::new(cols, template)),
raw: Storage::with_capacity(lines, cols),
max_scroll_limit,
display_offset: 0,
saved_cursor: Cursor::default(),
@ -203,10 +202,10 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
};
}
fn increase_scroll_limit(&mut self, count: usize, template: T) {
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, template, self.cols);
self.raw.initialize(count, self.cols);
}
}
@ -219,7 +218,11 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
#[inline]
pub fn scroll_down(&mut self, region: &Range<Line>, positions: Line, template: T) {
pub fn scroll_down<D>(&mut self, region: &Range<Line>, positions: Line)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
// Whether or not there is a scrolling region active, as long as it
// starts at the top, we can do a full rotation which just involves
// changing the start index.
@ -238,7 +241,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
// Finally, reset recycled lines.
for i in IndexRange(Line(0)..positions) {
self.raw[i].reset(template);
self.raw[i].reset(&self.cursor.template);
}
} else {
// Subregion rotation.
@ -247,7 +250,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
for line in IndexRange(region.start..(region.start + positions)) {
self.raw[line].reset(template);
self.raw[line].reset(&self.cursor.template);
}
}
}
@ -255,7 +258,11 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
/// Move lines at the bottom toward the top.
///
/// This is the performance-sensitive part of scrolling.
pub fn scroll_up(&mut self, region: &Range<Line>, positions: Line, template: T) {
pub fn scroll_up<D>(&mut self, region: &Range<Line>, positions: Line)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
let num_lines = self.screen_lines().0;
if region.start == Line(0) {
@ -264,7 +271,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
self.display_offset = min(self.display_offset + *positions, self.max_scroll_limit);
}
self.increase_scroll_limit(*positions, template);
self.increase_scroll_limit(*positions);
// Rotate the entire line buffer. If there's a scrolling region
// active, the bottom lines are restored in the next step.
@ -284,7 +291,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
//
// Recycled lines are just above the end of the scrolling region.
for i in 0..*positions {
self.raw[i + fixed_lines].reset(template);
self.raw[i + fixed_lines].reset(&self.cursor.template);
}
} else {
// Subregion rotation.
@ -294,12 +301,16 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
// Clear reused lines.
for line in IndexRange((region.end - positions)..region.end) {
self.raw[line].reset(template);
self.raw[line].reset(&self.cursor.template);
}
}
}
pub fn clear_viewport(&mut self, template: T) {
pub fn clear_viewport<D>(&mut self)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
// Determine how many lines to scroll up by.
let end = Point { line: 0, col: self.cols() };
let mut iter = self.iter_from(end);
@ -316,26 +327,30 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
self.display_offset = 0;
// Clear the viewport.
self.scroll_up(&region, positions, template);
self.scroll_up(&region, positions);
// Reset rotated lines.
for i in positions.0..self.lines.0 {
self.raw[i].reset(template);
self.raw[i].reset(&self.cursor.template);
}
}
/// Completely reset the grid state.
pub fn reset(&mut self, template: T) {
pub fn reset<D>(&mut self)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
self.clear_history();
// Reset all visible lines.
for row in 0..self.raw.len() {
self.raw[row].reset(template);
}
self.saved_cursor = Cursor::default();
self.cursor = Cursor::default();
self.display_offset = 0;
// Reset all visible lines.
for row in 0..self.raw.len() {
self.raw[row].reset(&self.cursor.template);
}
}
}
@ -372,15 +387,15 @@ impl<T> Grid<T> {
/// This is used only for initializing after loading ref-tests.
#[inline]
pub fn initialize_all(&mut self, template: T)
pub fn initialize_all(&mut self)
where
T: Copy + GridCell,
T: GridCell + Clone + Default,
{
// Remove all cached lines to clear them of any content.
self.truncate();
// Initialize everything with empty new lines.
self.raw.initialize(self.max_scroll_limit - self.history_size(), template, self.cols);
self.raw.initialize(self.max_scroll_limit - self.history_size(), self.cols);
}
/// This is used only for truncating before saving ref-tests.
@ -753,8 +768,8 @@ impl<'a, T: 'a> DisplayIter<'a, T> {
}
}
impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> {
type Item = Indexed<T>;
impl<'a, T: 'a> Iterator for DisplayIter<'a, T> {
type Item = Indexed<&'a T>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
@ -765,7 +780,7 @@ impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> {
// Get the next item.
let item = Some(Indexed {
inner: self.grid.raw[self.offset][self.col],
inner: &self.grid.raw[self.offset][self.col],
line: self.line,
column: self.col,
});

View File

@ -1,16 +1,24 @@
//! Grid resize and reflow.
use std::cmp::{min, Ordering};
use std::mem;
use crate::index::{Column, Line};
use crate::term::cell::Flags;
use crate::term::cell::{Flags, ResetDiscriminant};
use crate::grid::row::Row;
use crate::grid::{Dimensions, Grid, GridCell};
impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
/// Resize the grid's width and/or height.
pub fn resize(&mut self, reflow: bool, lines: Line, cols: Column) {
pub fn resize<D>(&mut self, reflow: bool, lines: Line, cols: Column)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
// Use empty template cell for resetting cells due to resize.
let template = mem::take(&mut self.cursor.template);
match self.lines.cmp(&lines) {
Ordering::Less => self.grow_lines(lines),
Ordering::Greater => self.shrink_lines(lines),
@ -22,6 +30,9 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
Ordering::Greater => self.shrink_cols(reflow, cols),
Ordering::Equal => (),
}
// Restore template cell.
self.cursor.template = template;
}
/// Add lines to the visible area.
@ -29,11 +40,15 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
/// 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: Line) {
fn grow_lines<D>(&mut self, new_line_count: Line)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
let lines_added = new_line_count - self.lines;
// Need to resize before updating buffer.
self.raw.grow_visible_lines(new_line_count, Row::new(self.cols, T::default()));
self.raw.grow_visible_lines(new_line_count);
self.lines = new_line_count;
let history_size = self.history_size();
@ -42,7 +57,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
// Move existing lines up for every line that couldn't be pulled from history.
if from_history != lines_added.0 {
let delta = lines_added - from_history;
self.scroll_up(&(Line(0)..new_line_count), delta, T::default());
self.scroll_up(&(Line(0)..new_line_count), delta);
}
// Move cursor down for every line pulled from history.
@ -60,11 +75,15 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
/// of the terminal window.
///
/// Alacritty takes the same approach.
fn shrink_lines(&mut self, target: Line) {
fn shrink_lines<D>(&mut self, target: Line)
where
T: ResetDiscriminant<D>,
D: PartialEq,
{
// Scroll up to keep content inside the window.
let required_scrolling = (self.cursor.point.line + 1).saturating_sub(target.0);
if required_scrolling > 0 {
self.scroll_up(&(Line(0)..self.lines), Line(required_scrolling), T::default());
self.scroll_up(&(Line(0)..self.lines), Line(required_scrolling));
// Clamp cursors to the new viewport size.
self.cursor.point.line = min(self.cursor.point.line, target - 1);
@ -194,7 +213,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
if reversed.len() < self.lines.0 {
let delta = self.lines.0 - reversed.len();
self.cursor.point.line.0 = self.cursor.point.line.saturating_sub(delta);
reversed.append(&mut vec![Row::new(cols, T::default()); delta]);
reversed.resize_with(self.lines.0, || Row::new(cols));
}
// Pull content down to put cursor in correct position, or move cursor up if there's no
@ -211,7 +230,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
let mut new_raw = Vec::with_capacity(reversed.len());
for mut row in reversed.drain(..).rev() {
if row.len() < cols.0 {
row.grow(cols, T::default());
row.grow(cols);
}
new_raw.push(row);
}
@ -269,11 +288,11 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
// Insert spacer if a wide char would be wrapped into the last column.
if row.len() >= cols.0 && row[cols - 1].flags().contains(Flags::WIDE_CHAR) {
wrapped.insert(0, row[cols - 1]);
let mut spacer = T::default();
spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER);
row[cols - 1] = spacer;
let wide_char = mem::replace(&mut row[cols - 1], spacer);
wrapped.insert(0, wide_char);
}
// Remove wide char spacer before shrinking.
@ -330,7 +349,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
// Make sure new row is at least as long as new width.
let occ = wrapped.len();
if occ < cols.0 {
wrapped.append(&mut vec![T::default(); cols.0 - occ]);
wrapped.resize_with(cols.0, T::default);
}
row = Row::from_vec(wrapped, occ);
}

View File

@ -3,12 +3,14 @@
use std::cmp::{max, min};
use std::ops::{Index, IndexMut};
use std::ops::{Range, RangeFrom, RangeFull, RangeTo, RangeToInclusive};
use std::ptr;
use std::slice;
use serde::{Deserialize, Serialize};
use crate::grid::GridCell;
use crate::index::Column;
use crate::term::cell::ResetDiscriminant;
/// A row in the grid.
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
@ -28,23 +30,44 @@ impl<T: PartialEq> PartialEq for Row<T> {
}
}
impl<T: Copy> Row<T> {
pub fn new(columns: Column, template: T) -> Row<T>
where
T: GridCell,
{
let occ = if template.is_empty() { 0 } else { columns.0 };
Row { inner: vec![template; columns.0], occ }
impl<T: Clone + Default> Row<T> {
/// Create a new terminal row.
///
/// Ideally the `template` should be `Copy` in all performance sensitive scenarios.
pub fn new(columns: Column) -> Row<T> {
debug_assert!(columns.0 >= 1);
let mut inner: Vec<T> = Vec::with_capacity(columns.0);
// This is a slightly optimized version of `std::vec::Vec::resize`.
unsafe {
let mut ptr = inner.as_mut_ptr();
for _ in 1..columns.0 {
ptr::write(ptr, T::default());
ptr = ptr.offset(1);
}
ptr::write(ptr, T::default());
inner.set_len(columns.0);
}
Row { inner, occ: 0 }
}
pub fn grow(&mut self, cols: Column, template: T) {
/// Increase the number of columns in the row.
#[inline]
pub fn grow(&mut self, cols: Column) {
if self.inner.len() >= cols.0 {
return;
}
self.inner.append(&mut vec![template; cols.0 - self.len()]);
self.inner.resize_with(cols.0, T::default);
}
/// Reduce the number of columns in the row.
///
/// This will return all non-empty cells that were removed.
pub fn shrink(&mut self, cols: Column) -> Option<Vec<T>>
where
T: GridCell,
@ -69,21 +92,22 @@ impl<T: Copy> Row<T> {
/// Reset all cells in the row to the `template` cell.
#[inline]
pub fn reset(&mut self, template: T)
pub fn reset<D>(&mut self, template: &T)
where
T: GridCell + PartialEq,
T: ResetDiscriminant<D> + GridCell,
D: PartialEq,
{
debug_assert!(!self.inner.is_empty());
// Mark all cells as dirty if template cell changed.
let len = self.inner.len();
if !self.inner[len - 1].fast_eq(template) {
if self.inner[len - 1].discriminant() != template.discriminant() {
self.occ = len;
}
// Reset every dirty in the row.
for item in &mut self.inner[..self.occ] {
*item = template;
// Reset every dirty cell in the row.
for item in &mut self.inner[0..self.occ] {
item.reset(template);
}
self.occ = 0;

View File

@ -5,7 +5,6 @@ use std::ops::{Index, IndexMut};
use serde::{Deserialize, Serialize};
use super::Row;
use crate::grid::GridCell;
use crate::index::{Column, Line};
/// Maximum number of buffered lines outside of the grid for performance optimization.
@ -27,7 +26,7 @@ const MAX_CACHE_SIZE: usize = 1_000;
/// [`slice::rotate_left`]: https://doc.rust-lang.org/std/primitive.slice.html#method.rotate_left
/// [`Deref`]: std::ops::Deref
/// [`zero`]: #structfield.zero
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Storage<T> {
inner: Vec<Row<T>>,
@ -62,57 +61,35 @@ impl<T: PartialEq> PartialEq for Storage<T> {
impl<T> Storage<T> {
#[inline]
pub fn with_capacity(visible_lines: Line, template: Row<T>) -> Storage<T>
pub fn with_capacity(visible_lines: Line, cols: Column) -> Storage<T>
where
T: Clone,
T: Clone + Default,
{
// Initialize visible lines, the scrollback buffer is initialized dynamically.
let inner = vec![template; visible_lines.0];
// Initialize visible lines; the scrollback buffer is initialized dynamically.
let mut inner = Vec::with_capacity(visible_lines.0);
inner.resize_with(visible_lines.0, || Row::new(cols));
Storage { inner, zero: 0, visible_lines, len: visible_lines.0 }
}
/// Increase the number of lines in the buffer.
pub fn grow_visible_lines(&mut self, next: Line, template_row: Row<T>)
#[inline]
pub fn grow_visible_lines(&mut self, next: Line)
where
T: Clone,
T: Clone + Default,
{
// Number of lines the buffer needs to grow.
let growage = next - self.visible_lines;
self.grow_lines(growage.0, template_row);
let cols = self[0].len();
self.initialize(growage.0, Column(cols));
// Update visible lines.
self.visible_lines = next;
}
/// Grow the number of lines in the buffer, filling new lines with the template.
fn grow_lines(&mut self, growage: usize, template_row: Row<T>)
where
T: Clone,
{
// Only grow if there are not enough lines still hidden.
let mut new_growage = 0;
if growage > (self.inner.len() - self.len) {
// Lines to grow additionally to invisible lines.
new_growage = growage - (self.inner.len() - self.len);
// Split off the beginning of the raw inner buffer.
let mut start_buffer = self.inner.split_off(self.zero);
// Insert new template rows at the end of the raw inner buffer.
let mut new_lines = vec![template_row; new_growage];
self.inner.append(&mut new_lines);
// Add the start to the raw inner buffer again.
self.inner.append(&mut start_buffer);
}
// Update raw buffer length and zero offset.
self.zero += new_growage;
self.len += growage;
}
/// Decrease the number of lines in the buffer.
#[inline]
pub fn shrink_visible_lines(&mut self, next: Line) {
// Shrink the size without removing any lines.
let shrinkage = self.visible_lines - next;
@ -123,6 +100,7 @@ impl<T> Storage<T> {
}
/// Shrink the number of lines in the buffer.
#[inline]
pub fn shrink_lines(&mut self, shrinkage: usize) {
self.len -= shrinkage;
@ -133,26 +111,24 @@ impl<T> Storage<T> {
}
/// Truncate the invisible elements from the raw buffer.
#[inline]
pub fn truncate(&mut self) {
self.inner.rotate_left(self.zero);
self.inner.truncate(self.len);
self.rezero();
self.zero = 0;
self.inner.truncate(self.len);
}
/// Dynamically grow the storage buffer at runtime.
#[inline]
pub fn initialize(&mut self, additional_rows: usize, template: T, cols: Column)
pub fn initialize(&mut self, additional_rows: usize, cols: Column)
where
T: GridCell + Copy,
T: Clone + Default,
{
if self.len + additional_rows > self.inner.len() {
let realloc_size = max(additional_rows, MAX_CACHE_SIZE);
let mut new = vec![Row::new(cols, template); realloc_size];
let mut split = self.inner.split_off(self.zero);
self.inner.append(&mut new);
self.inner.append(&mut split);
self.zero += realloc_size;
self.rezero();
let realloc_size = self.inner.len() + max(additional_rows, MAX_CACHE_SIZE);
self.inner.resize_with(realloc_size, || Row::new(cols));
}
self.len += additional_rows;
@ -163,24 +139,7 @@ impl<T> Storage<T> {
self.len
}
/// Compute actual index in underlying storage given the requested index.
#[inline]
fn compute_index(&self, requested: usize) -> usize {
debug_assert!(requested < self.len);
let zeroed = self.zero + requested;
// Use if/else instead of remainder here to improve performance.
//
// Requires `zeroed` to be smaller than `self.inner.len() * 2`,
// but both `self.zero` and `requested` are always smaller than `self.inner.len()`.
if zeroed >= self.inner.len() {
zeroed - self.inner.len()
} else {
zeroed
}
}
pub fn swap_lines(&mut self, a: Line, b: Line) {
let offset = self.inner.len() + self.zero + *self.visible_lines - 1;
let a = (offset - *a) % self.inner.len();
@ -256,11 +215,39 @@ impl<T> Storage<T> {
let mut buffer = Vec::new();
mem::swap(&mut buffer, &mut self.inner);
self.zero = 0;
self.len = 0;
buffer
}
/// Compute actual index in underlying storage given the requested index.
#[inline]
fn compute_index(&self, requested: usize) -> usize {
debug_assert!(requested < self.len);
let zeroed = self.zero + requested;
// Use if/else instead of remainder here to improve performance.
//
// Requires `zeroed` to be smaller than `self.inner.len() * 2`,
// but both `self.zero` and `requested` are always smaller than `self.inner.len()`.
if zeroed >= self.inner.len() {
zeroed - self.inner.len()
} else {
zeroed
}
}
/// Rotate the ringbuffer to reset `self.zero` back to index `0`.
#[inline]
fn rezero(&mut self) {
if self.zero == 0 {
return;
}
self.inner.rotate_left(self.zero);
self.zero = 0;
}
}
impl<T> Index<usize> for Storage<T> {
@ -311,6 +298,10 @@ mod tests {
*self == ' ' || *self == '\t'
}
fn reset(&mut self, template: &Self) {
*self = *template;
}
fn flags(&self) -> &Flags {
unimplemented!();
}
@ -318,15 +309,11 @@ mod tests {
fn flags_mut(&mut self) -> &mut Flags {
unimplemented!();
}
fn fast_eq(&self, other: Self) -> bool {
self == &other
}
}
#[test]
fn with_capacity() {
let storage = Storage::with_capacity(Line(3), Row::new(Column(0), ' '));
let storage = Storage::<char>::with_capacity(Line(3), Column(1));
assert_eq!(storage.inner.len(), 3);
assert_eq!(storage.len, 3);
@ -336,33 +323,33 @@ mod tests {
#[test]
fn indexing() {
let mut storage = Storage::with_capacity(Line(3), Row::new(Column(0), ' '));
let mut storage = Storage::<char>::with_capacity(Line(3), Column(1));
storage[0] = Row::new(Column(1), '0');
storage[1] = Row::new(Column(1), '1');
storage[2] = Row::new(Column(1), '2');
storage[0] = filled_row('0');
storage[1] = filled_row('1');
storage[2] = filled_row('2');
assert_eq!(storage[0], Row::new(Column(1), '0'));
assert_eq!(storage[1], Row::new(Column(1), '1'));
assert_eq!(storage[2], Row::new(Column(1), '2'));
assert_eq!(storage[0], filled_row('0'));
assert_eq!(storage[1], filled_row('1'));
assert_eq!(storage[2], filled_row('2'));
storage.zero += 1;
assert_eq!(storage[0], Row::new(Column(1), '1'));
assert_eq!(storage[1], Row::new(Column(1), '2'));
assert_eq!(storage[2], Row::new(Column(1), '0'));
assert_eq!(storage[0], filled_row('1'));
assert_eq!(storage[1], filled_row('2'));
assert_eq!(storage[2], filled_row('0'));
}
#[test]
#[should_panic]
fn indexing_above_inner_len() {
let storage = Storage::with_capacity(Line(1), Row::new(Column(0), ' '));
let storage = Storage::<char>::with_capacity(Line(1), Column(1));
let _ = &storage[2];
}
#[test]
fn rotate() {
let mut storage = Storage::with_capacity(Line(3), Row::new(Column(0), ' '));
let mut storage = Storage::<char>::with_capacity(Line(3), Column(1));
storage.rotate(2);
assert_eq!(storage.zero, 2);
storage.shrink_lines(2);
@ -378,39 +365,34 @@ mod tests {
/// 1: 1
/// 2: -
/// After:
/// 0: -
/// 1: 0 <- Zero
/// 2: 1
/// 3: -
/// 0: 0 <- Zero
/// 1: 1
/// 2: -
/// 3: \0
/// ...
/// MAX_CACHE_SIZE: \0
#[test]
fn grow_after_zero() {
// Setup storage area
let mut storage = Storage {
inner: vec![
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '-'),
],
// Setup storage area.
let mut storage: Storage<char> = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('-')],
zero: 0,
visible_lines: Line(3),
len: 3,
};
// Grow buffer
storage.grow_visible_lines(Line(4), Row::new(Column(1), '-'));
// Grow buffer.
storage.grow_visible_lines(Line(4));
// Make sure the result is correct
let expected = Storage {
inner: vec![
Row::new(Column(1), '-'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '-'),
],
zero: 1,
// Make sure the result is correct.
let mut expected = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('-')],
zero: 0,
visible_lines: Line(4),
len: 4,
};
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.zero, expected.zero);
@ -424,39 +406,34 @@ mod tests {
/// 1: 0 <- Zero
/// 2: 1
/// After:
/// 0: -
/// 1: -
/// 2: 0 <- Zero
/// 3: 1
/// 0: 0 <- Zero
/// 1: 1
/// 2: -
/// 3: \0
/// ...
/// MAX_CACHE_SIZE: \0
#[test]
fn grow_before_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
Row::new(Column(1), '-'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
],
let mut storage: Storage<char> = Storage {
inner: vec![filled_row('-'), filled_row('0'), filled_row('1')],
zero: 1,
visible_lines: Line(3),
len: 3,
};
// Grow buffer.
storage.grow_visible_lines(Line(4), Row::new(Column(1), '-'));
storage.grow_visible_lines(Line(4));
// Make sure the result is correct.
let expected = Storage {
inner: vec![
Row::new(Column(1), '-'),
Row::new(Column(1), '-'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
],
zero: 2,
let mut expected = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('-')],
zero: 0,
visible_lines: Line(4),
len: 4,
};
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.zero, expected.zero);
@ -476,12 +453,8 @@ mod tests {
#[test]
fn shrink_before_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
Row::new(Column(1), '2'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
],
let mut storage: Storage<char> = Storage {
inner: vec![filled_row('2'), filled_row('0'), filled_row('1')],
zero: 1,
visible_lines: Line(3),
len: 3,
@ -492,11 +465,7 @@ mod tests {
// Make sure the result is correct.
let expected = Storage {
inner: vec![
Row::new(Column(1), '2'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
],
inner: vec![filled_row('2'), filled_row('0'), filled_row('1')],
zero: 1,
visible_lines: Line(2),
len: 2,
@ -520,12 +489,8 @@ mod tests {
#[test]
fn shrink_after_zero() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
],
let mut storage: Storage<char> = Storage {
inner: vec![filled_row('0'), filled_row('1'), filled_row('2')],
zero: 0,
visible_lines: Line(3),
len: 3,
@ -536,11 +501,7 @@ mod tests {
// Make sure the result is correct.
let expected = Storage {
inner: vec![
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
],
inner: vec![filled_row('0'), filled_row('1'), filled_row('2')],
zero: 0,
visible_lines: Line(2),
len: 2,
@ -570,14 +531,14 @@ mod tests {
#[test]
fn shrink_before_and_after_zero() {
// Setup storage area.
let mut storage = Storage {
let mut storage: Storage<char> = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 2,
visible_lines: Line(6),
@ -590,12 +551,12 @@ mod tests {
// Make sure the result is correct.
let expected = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 2,
visible_lines: Line(2),
@ -622,14 +583,14 @@ mod tests {
#[test]
fn truncate_invisible_lines() {
// Setup storage area.
let mut storage = Storage {
let mut storage: Storage<char> = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 2,
visible_lines: Line(1),
@ -641,7 +602,7 @@ mod tests {
// Make sure the result is correct.
let expected = Storage {
inner: vec![Row::new(Column(1), '0'), Row::new(Column(1), '1')],
inner: vec![filled_row('0'), filled_row('1')],
zero: 0,
visible_lines: Line(1),
len: 2,
@ -664,12 +625,8 @@ mod tests {
#[test]
fn truncate_invisible_lines_beginning() {
// Setup storage area.
let mut storage = Storage {
inner: vec![
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '0'),
],
let mut storage: Storage<char> = Storage {
inner: vec![filled_row('1'), filled_row('2'), filled_row('0')],
zero: 2,
visible_lines: Line(1),
len: 2,
@ -680,7 +637,7 @@ mod tests {
// Make sure the result is correct.
let expected = Storage {
inner: vec![Row::new(Column(1), '0'), Row::new(Column(1), '1')],
inner: vec![filled_row('0'), filled_row('1')],
zero: 0,
visible_lines: Line(1),
len: 2,
@ -718,14 +675,14 @@ mod tests {
#[test]
fn shrink_then_grow() {
// Setup storage area.
let mut storage = Storage {
let mut storage: Storage<char> = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 2,
visible_lines: Line(0),
@ -738,12 +695,12 @@ mod tests {
// Make sure the result after shrinking is correct.
let shrinking_expected = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 2,
visible_lines: Line(0),
@ -754,23 +711,23 @@ mod tests {
assert_eq!(storage.len, shrinking_expected.len);
// Grow buffer.
storage.grow_lines(4, Row::new(Column(1), '-'));
storage.initialize(1, Column(1));
// Make sure the result after shrinking is correct.
// Make sure the previously freed elements are reused.
let growing_expected = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '-'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 3,
zero: 2,
visible_lines: Line(0),
len: 7,
len: 4,
};
assert_eq!(storage.inner, growing_expected.inner);
assert_eq!(storage.zero, growing_expected.zero);
assert_eq!(storage.len, growing_expected.len);
@ -779,14 +736,14 @@ mod tests {
#[test]
fn initialize() {
// Setup storage area.
let mut storage = Storage {
let mut storage: Storage<char> = Storage {
inner: vec![
Row::new(Column(1), '4'),
Row::new(Column(1), '5'),
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
filled_row('4'),
filled_row('5'),
filled_row('0'),
filled_row('1'),
filled_row('2'),
filled_row('3'),
],
zero: 2,
visible_lines: Line(0),
@ -795,39 +752,31 @@ mod tests {
// Initialize additional lines.
let init_size = 3;
storage.initialize(init_size, '-', Column(1));
// Make sure the lines are present and at the right location.
storage.initialize(init_size, Column(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);
let mut expected_inner = vec![Row::new(Column(1), '4'), Row::new(Column(1), '5')];
expected_inner.append(&mut vec![Row::new(Column(1), '-'); expected_init_size]);
expected_inner.append(&mut vec![
Row::new(Column(1), '0'),
Row::new(Column(1), '1'),
Row::new(Column(1), '2'),
Row::new(Column(1), '3'),
]);
let expected_storage = Storage {
inner: expected_inner,
zero: 2 + expected_init_size,
visible_lines: Line(0),
len: 9,
};
expected_inner.append(&mut vec![filled_row('\0'); expected_init_size]);
let expected_storage =
Storage { inner: expected_inner, zero: 0, visible_lines: Line(0), len: 9 };
assert_eq!(storage.inner, expected_storage.inner);
assert_eq!(storage.zero, expected_storage.zero);
assert_eq!(storage.len, expected_storage.len);
assert_eq!(storage.zero, expected_storage.zero);
assert_eq!(storage.inner, expected_storage.inner);
}
#[test]
fn rotate_wrap_zero() {
let mut storage = Storage {
inner: vec![
Row::new(Column(1), '-'),
Row::new(Column(1), '-'),
Row::new(Column(1), '-'),
],
let mut storage: Storage<char> = Storage {
inner: vec![filled_row('-'), filled_row('-'), filled_row('-')],
zero: 2,
visible_lines: Line(0),
len: 3,
@ -837,4 +786,10 @@ mod tests {
assert!(storage.zero < storage.inner.len());
}
fn filled_row(content: char) -> Row<char> {
let mut row = Row::new(Column(1));
row[Column(0)] = content;
row
}
}

View File

@ -1,14 +1,18 @@
//! Tests for the Grid.
use super::{BidirectionalIterator, Dimensions, Grid, GridCell};
use crate::index::{Column, Line, Point};
use crate::term::cell::{Cell, Flags};
use super::*;
use crate::term::cell::Cell;
impl GridCell for usize {
fn is_empty(&self) -> bool {
*self == 0
}
fn reset(&mut self, template: &Self) {
*self = *template;
}
fn flags(&self) -> &Flags {
unimplemented!();
}
@ -16,15 +20,11 @@ impl GridCell for usize {
fn flags_mut(&mut self) -> &mut Flags {
unimplemented!();
}
fn fast_eq(&self, other: Self) -> bool {
self == &other
}
}
#[test]
fn grid_clamp_buffer_point() {
let mut grid = Grid::new(Line(10), Column(10), 1_000, 0);
let mut grid = Grid::<usize>::new(Line(10), Column(10), 1_000);
grid.display_offset = 5;
let point = grid.clamp_buffer_to_visible(Point::new(10, Column(3)));
@ -44,7 +44,7 @@ fn grid_clamp_buffer_point() {
#[test]
fn visible_to_buffer() {
let mut grid = Grid::new(Line(10), Column(10), 1_000, 0);
let mut grid = Grid::<usize>::new(Line(10), Column(10), 1_000);
grid.display_offset = 5;
let point = grid.visible_to_buffer(Point::new(Line(4), Column(3)));
@ -59,12 +59,12 @@ fn visible_to_buffer() {
// Scroll up moves lines upward.
#[test]
fn scroll_up() {
let mut grid = Grid::new(Line(10), Column(1), 0, 0);
let mut grid = Grid::<usize>::new(Line(10), Column(1), 0);
for i in 0..10 {
grid[Line(i)][Column(0)] = i;
}
grid.scroll_up(&(Line(0)..Line(10)), Line(2), 0);
grid.scroll_up::<usize>(&(Line(0)..Line(10)), Line(2));
assert_eq!(grid[Line(0)][Column(0)], 2);
assert_eq!(grid[Line(0)].occ, 1);
@ -91,12 +91,12 @@ fn scroll_up() {
// Scroll down moves lines downward.
#[test]
fn scroll_down() {
let mut grid = Grid::new(Line(10), Column(1), 0, 0);
let mut grid = Grid::<usize>::new(Line(10), Column(1), 0);
for i in 0..10 {
grid[Line(i)][Column(0)] = i;
}
grid.scroll_down(&(Line(0)..Line(10)), Line(2), 0);
grid.scroll_down::<usize>(&(Line(0)..Line(10)), Line(2));
assert_eq!(grid[Line(0)][Column(0)], 0); // was 8.
assert_eq!(grid[Line(0)].occ, 0);
@ -123,7 +123,7 @@ fn scroll_down() {
// Test that GridIterator works.
#[test]
fn test_iter() {
let mut grid = Grid::new(Line(5), Column(5), 0, 0);
let mut grid = Grid::<usize>::new(Line(5), Column(5), 0);
for i in 0..5 {
for j in 0..5 {
grid[Line(i)][Column(j)] = i * 5 + j;
@ -161,7 +161,7 @@ fn test_iter() {
#[test]
fn shrink_reflow() {
let mut grid = Grid::new(Line(1), Column(5), 2, cell('x'));
let mut grid = Grid::<Cell>::new(Line(1), Column(5), 2);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = cell('2');
grid[Line(0)][Column(2)] = cell('3');
@ -187,7 +187,7 @@ fn shrink_reflow() {
#[test]
fn shrink_reflow_twice() {
let mut grid = Grid::new(Line(1), Column(5), 2, cell('x'));
let mut grid = Grid::<Cell>::new(Line(1), Column(5), 2);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = cell('2');
grid[Line(0)][Column(2)] = cell('3');
@ -214,7 +214,7 @@ fn shrink_reflow_twice() {
#[test]
fn shrink_reflow_empty_cell_inside_line() {
let mut grid = Grid::new(Line(1), Column(5), 3, cell('x'));
let mut grid = Grid::<Cell>::new(Line(1), Column(5), 3);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = Cell::default();
grid[Line(0)][Column(2)] = cell('3');
@ -252,7 +252,7 @@ fn shrink_reflow_empty_cell_inside_line() {
#[test]
fn grow_reflow() {
let mut grid = Grid::new(Line(2), Column(2), 0, cell('x'));
let mut grid = Grid::<Cell>::new(Line(2), Column(2), 0);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = wrap_cell('2');
grid[Line(1)][Column(0)] = cell('3');
@ -276,7 +276,7 @@ fn grow_reflow() {
#[test]
fn grow_reflow_multiline() {
let mut grid = Grid::new(Line(3), Column(2), 0, cell('x'));
let mut grid = Grid::<Cell>::new(Line(3), Column(2), 0);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = wrap_cell('2');
grid[Line(1)][Column(0)] = cell('3');
@ -309,7 +309,7 @@ fn grow_reflow_multiline() {
#[test]
fn grow_reflow_disabled() {
let mut grid = Grid::new(Line(2), Column(2), 0, cell('x'));
let mut grid = Grid::<Cell>::new(Line(2), Column(2), 0);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = wrap_cell('2');
grid[Line(1)][Column(0)] = cell('3');
@ -332,7 +332,7 @@ fn grow_reflow_disabled() {
#[test]
fn shrink_reflow_disabled() {
let mut grid = Grid::new(Line(1), Column(5), 2, cell('x'));
let mut grid = Grid::<Cell>::new(Line(1), Column(5), 2);
grid[Line(0)][Column(0)] = cell('1');
grid[Line(0)][Column(1)] = cell('2');
grid[Line(0)][Column(2)] = cell('3');

View File

@ -190,8 +190,8 @@ impl From<Point> for Point<usize> {
}
}
impl From<RenderableCell> for Point<Line> {
fn from(cell: RenderableCell) -> Self {
impl From<&RenderableCell> for Point<Line> {
fn from(cell: &RenderableCell) -> Self {
Point::new(cell.line, cell.column)
}
}

View File

@ -1,14 +1,12 @@
use bitflags::bitflags;
use std::boxed::Box;
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use crate::ansi::{Color, NamedColor};
use crate::grid::{self, GridCell};
use crate::index::Column;
/// Maximum number of zerowidth characters which will be stored per cell.
pub const MAX_ZEROWIDTH_CHARS: usize = 5;
bitflags! {
#[derive(Serialize, Deserialize)]
pub struct Flags: u16 {
@ -29,23 +27,77 @@ bitflags! {
}
}
const fn default_extra() -> [char; MAX_ZEROWIDTH_CHARS] {
[' '; MAX_ZEROWIDTH_CHARS]
/// Trait for determining if a reset should be performed.
pub trait ResetDiscriminant<T> {
/// Value based on which equality for the reset will be determined.
fn discriminant(&self) -> T;
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
impl<T: Copy> ResetDiscriminant<T> for T {
fn discriminant(&self) -> T {
*self
}
}
impl ResetDiscriminant<Color> for Cell {
fn discriminant(&self) -> Color {
self.bg
}
}
/// Dynamically allocated cell content.
///
/// This storage is reserved for cell attributes which are rarely set. This allows reducing the
/// allocation required ahead of time for every cell, with some additional overhead when the extra
/// storage is actually required.
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
struct CellExtra {
zerowidth: Vec<char>,
}
/// Content and attributes of a single cell in the terminal grid.
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct Cell {
pub c: char,
pub fg: Color,
pub bg: Color,
pub flags: Flags,
#[serde(default = "default_extra")]
pub extra: [char; MAX_ZEROWIDTH_CHARS],
#[serde(default)]
extra: Option<Box<CellExtra>>,
}
impl Default for Cell {
#[inline]
fn default() -> Cell {
Cell::new(' ', Color::Named(NamedColor::Foreground), Color::Named(NamedColor::Background))
Cell {
c: ' ',
bg: Color::Named(NamedColor::Background),
fg: Color::Named(NamedColor::Foreground),
flags: Flags::empty(),
extra: None,
}
}
}
impl Cell {
/// Zerowidth characters stored in this cell.
#[inline]
pub fn zerowidth(&self) -> Option<&[char]> {
self.extra.as_ref().map(|extra| extra.zerowidth.as_slice())
}
/// Write a new zerowidth character to this cell.
#[inline]
pub fn push_zerowidth(&mut self, c: char) {
self.extra.get_or_insert_with(Default::default).zerowidth.push(c);
}
/// Free all dynamically allocated cell storage.
#[inline]
pub fn drop_extra(&mut self) {
if self.extra.is_some() {
self.extra = None;
}
}
}
@ -53,7 +105,6 @@ impl GridCell for Cell {
#[inline]
fn is_empty(&self) -> bool {
(self.c == ' ' || self.c == '\t')
&& self.extra[0] == ' '
&& self.bg == Color::Named(NamedColor::Background)
&& self.fg == Color::Named(NamedColor::Foreground)
&& !self.flags.intersects(
@ -65,6 +116,7 @@ impl GridCell for Cell {
| Flags::WIDE_CHAR_SPACER
| Flags::LEADING_WIDE_CHAR_SPACER,
)
&& self.extra.as_ref().map(|extra| extra.zerowidth.is_empty()) != Some(false)
}
#[inline]
@ -78,8 +130,15 @@ impl GridCell for Cell {
}
#[inline]
fn fast_eq(&self, other: Self) -> bool {
self.bg == other.bg
fn reset(&mut self, template: &Self) {
*self = Cell { bg: template.bg, ..Cell::default() };
}
}
impl From<Color> for Cell {
#[inline]
fn from(color: Color) -> Self {
Self { bg: color, ..Cell::default() }
}
}
@ -98,7 +157,9 @@ impl LineLength for grid::Row<Cell> {
}
for (index, cell) in self[..].iter().rev().enumerate() {
if cell.c != ' ' || cell.extra[0] != ' ' {
if cell.c != ' '
|| cell.extra.as_ref().map(|extra| extra.zerowidth.is_empty()) == Some(false)
{
length = Column(self.len() - index);
break;
}
@ -108,57 +169,6 @@ impl LineLength for grid::Row<Cell> {
}
}
impl Cell {
#[inline]
pub fn bold(&self) -> bool {
self.flags.contains(Flags::BOLD)
}
#[inline]
pub fn inverse(&self) -> bool {
self.flags.contains(Flags::INVERSE)
}
#[inline]
pub fn dim(&self) -> bool {
self.flags.contains(Flags::DIM)
}
pub fn new(c: char, fg: Color, bg: Color) -> Cell {
Cell { extra: [' '; MAX_ZEROWIDTH_CHARS], c, bg, fg, flags: Flags::empty() }
}
#[inline]
pub fn reset(&mut self, template: &Cell) {
// memcpy template to self.
*self = Cell { c: template.c, bg: template.bg, ..Cell::default() };
}
#[inline]
pub fn chars(&self) -> [char; MAX_ZEROWIDTH_CHARS + 1] {
unsafe {
let mut chars = [std::mem::MaybeUninit::uninit(); MAX_ZEROWIDTH_CHARS + 1];
std::ptr::write(chars[0].as_mut_ptr(), self.c);
std::ptr::copy_nonoverlapping(
self.extra.as_ptr() as *mut std::mem::MaybeUninit<char>,
chars.as_mut_ptr().offset(1),
self.extra.len(),
);
std::mem::transmute(chars)
}
}
#[inline]
pub fn push_extra(&mut self, c: char) {
for elem in self.extra.iter_mut() {
if elem == &' ' {
*elem = c;
break;
}
}
}
}
#[cfg(test)]
mod tests {
use super::{Cell, LineLength};
@ -168,8 +178,7 @@ mod tests {
#[test]
fn line_length_works() {
let template = Cell::default();
let mut row = Row::new(Column(10), template);
let mut row = Row::<Cell>::new(Column(10));
row[Column(5)].c = 'a';
assert_eq!(row.line_length(), Column(6));
@ -177,8 +186,7 @@ mod tests {
#[test]
fn line_length_works_with_wrapline() {
let template = Cell::default();
let mut row = Row::new(Column(10), template);
let mut row = Row::<Cell>::new(Column(10));
row[Column(9)].flags.insert(super::Flags::WRAPLINE);
assert_eq!(row.line_length(), Column(10));
@ -188,7 +196,8 @@ mod tests {
#[cfg(all(test, feature = "bench"))]
mod benches {
extern crate test;
use super::Cell;
use super::*;
#[bench]
fn cell_reset(b: &mut test::Bencher) {
@ -196,7 +205,7 @@ mod benches {
let mut cell = Cell::default();
for _ in 0..100 {
cell.reset(test::black_box(&Cell::default()));
cell = test::black_box(Color::Named(NamedColor::Foreground).into());
}
test::black_box(cell);

View File

@ -218,7 +218,7 @@ impl<'a, C> RenderableCellsIter<'a, C> {
// Convert to absolute coordinates to adjust for the display offset.
let buffer_point = self.grid.visible_to_buffer(point);
let cell = self.grid[buffer_point];
let cell = &self.grid[buffer_point];
// Check if wide char's spacers are selected.
if cell.flags.contains(Flags::WIDE_CHAR) {
@ -249,13 +249,13 @@ impl<'a, C> RenderableCellsIter<'a, C> {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RenderableCellContent {
Chars([char; cell::MAX_ZEROWIDTH_CHARS + 1]),
Chars((char, Option<Vec<char>>)),
Cursor(CursorKey),
}
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct RenderableCell {
/// A _Display_ line (not necessarily an _Active_ line).
pub line: Line,
@ -268,14 +268,14 @@ pub struct RenderableCell {
}
impl RenderableCell {
fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<Cell>) -> Self {
fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<&Cell>) -> Self {
let point = Point::new(cell.line, cell.column);
// Lookup RGB values.
let mut fg_rgb = Self::compute_fg_rgb(iter.config, iter.colors, cell.fg, cell.flags);
let mut bg_rgb = Self::compute_bg_rgb(iter.colors, cell.bg);
let mut bg_alpha = if cell.inverse() {
let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) {
mem::swap(&mut fg_rgb, &mut bg_rgb);
1.0
} else {
@ -308,10 +308,12 @@ impl RenderableCell {
}
}
let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec());
RenderableCell {
line: cell.line,
column: cell.column,
inner: RenderableCellContent::Chars(cell.chars()),
inner: RenderableCellContent::Chars((cell.c, zerowidth)),
fg: fg_rgb,
bg: bg_rgb,
bg_alpha,
@ -322,7 +324,7 @@ impl RenderableCell {
fn is_empty(&self) -> bool {
self.bg_alpha == 0.
&& !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE)
&& self.inner == RenderableCellContent::Chars([' '; cell::MAX_ZEROWIDTH_CHARS + 1])
&& self.inner == RenderableCellContent::Chars((' ', None))
}
fn compute_fg_rgb<C>(config: &Config<C>, colors: &color::List, fg: Color, flags: Flags) -> Rgb {
@ -425,7 +427,7 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> {
let buffer_point = self.grid.visible_to_buffer(self.cursor.point);
let cell = Indexed {
inner: self.grid[buffer_point.line][buffer_point.col],
inner: &self.grid[buffer_point.line][buffer_point.col],
column: self.cursor.point.col,
line: self.cursor.point.line,
};
@ -851,8 +853,8 @@ impl<T> Term<T> {
let num_lines = size.screen_lines;
let history_size = config.scrolling.history() as usize;
let grid = Grid::new(num_lines, num_cols, history_size, Cell::default());
let alt = Grid::new(num_lines, num_cols, 0 /* scroll history */, Cell::default());
let grid = Grid::new(num_lines, num_cols, history_size);
let alt = Grid::new(num_lines, num_cols, 0);
let tabs = TabStops::new(grid.cols());
@ -979,7 +981,7 @@ impl<T> Term<T> {
let mut tab_mode = false;
for col in IndexRange::from(cols.start..line_length) {
let cell = grid_line[col];
let cell = &grid_line[col];
// Skip over cells until next tab-stop once a tab was found.
if tab_mode {
@ -999,7 +1001,7 @@ impl<T> Term<T> {
text.push(cell.c);
// Push zero-width characters.
for c in (&cell.chars()[1..]).iter().take_while(|c| **c != ' ') {
for c in cell.zerowidth().into_iter().flatten() {
text.push(*c);
}
}
@ -1111,14 +1113,14 @@ impl<T> Term<T> {
pub fn swap_alt(&mut self) {
if !self.mode.contains(TermMode::ALT_SCREEN) {
// Set alt screen cursor to the current primary screen cursor.
self.inactive_grid.cursor = self.grid.cursor;
self.inactive_grid.cursor = self.grid.cursor.clone();
// Drop information about the primary screens saved cursor.
self.grid.saved_cursor = self.grid.cursor;
self.grid.saved_cursor = self.grid.cursor.clone();
// Reset alternate screen contents.
let template = self.inactive_grid.cursor.template;
self.inactive_grid.region_mut(..).each(|c| c.reset(&template));
let bg = self.inactive_grid.cursor.template.bg;
self.inactive_grid.region_mut(..).each(|cell| *cell = bg.into());
}
mem::swap(&mut self.grid, &mut self.inactive_grid);
@ -1149,8 +1151,7 @@ impl<T> Term<T> {
.and_then(|s| s.rotate(self, &absolute_region, -(lines.0 as isize)));
// Scroll between origin and bottom
let template = Cell { bg: self.grid.cursor.template.bg, ..Cell::default() };
self.grid.scroll_down(&region, lines, template);
self.grid.scroll_down(&region, lines);
}
/// Scroll screen up
@ -1173,8 +1174,7 @@ impl<T> Term<T> {
self.selection.take().and_then(|s| s.rotate(self, &absolute_region, lines.0 as isize));
// Scroll from origin to bottom less number of lines.
let template = Cell { bg: self.grid.cursor.template.bg, ..Cell::default() };
self.grid.scroll_up(&region, lines, template);
self.grid.scroll_up(&region, lines);
}
fn deccolm(&mut self)
@ -1186,8 +1186,8 @@ impl<T> Term<T> {
self.set_scrolling_region(1, None);
// Clear grid.
let template = self.grid.cursor.template;
self.grid.region_mut(..).each(|c| c.reset(&template));
let bg = self.grid.cursor.template.bg;
self.grid.region_mut(..).each(|cell| *cell = bg.into());
}
#[inline]
@ -1355,16 +1355,24 @@ impl<T> Term<T> {
}
/// Write `c` to the cell at the cursor position.
#[inline]
#[inline(always)]
fn write_at_cursor(&mut self, c: char) -> &mut Cell
where
T: EventListener,
{
let mut cell = self.grid.cursor.template;
cell.c = self.grid.cursor.charsets[self.active_charset].map(c);
let c = self.grid.cursor.charsets[self.active_charset].map(c);
let fg = self.grid.cursor.template.fg;
let bg = self.grid.cursor.template.bg;
let flags = self.grid.cursor.template.flags;
let cursor_cell = self.grid.cursor_cell();
*cursor_cell = cell;
cursor_cell.drop_extra();
cursor_cell.c = c;
cursor_cell.fg = fg;
cursor_cell.bg = bg;
cursor_cell.flags = flags;
cursor_cell
}
@ -1411,7 +1419,7 @@ impl<T> Term<T> {
// Expand across wide cell when inside wide char or spacer.
let buffer_point = self.visible_to_buffer(point);
let cell = self.grid[buffer_point.line][buffer_point.col];
let cell = &self.grid[buffer_point.line][buffer_point.col];
let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
point.col -= 1;
true
@ -1448,7 +1456,7 @@ impl<T> Dimensions for Term<T> {
impl<T: EventListener> Handler for Term<T> {
/// A character to be displayed.
#[inline]
#[inline(never)]
fn input(&mut self, c: char) {
// Number of cells the char will occupy.
let width = match c.width() {
@ -1463,7 +1471,7 @@ impl<T: EventListener> Handler for Term<T> {
if self.grid[line][Column(col)].flags.contains(Flags::WIDE_CHAR_SPACER) {
col = col.saturating_sub(1);
}
self.grid[line][Column(col)].push_extra(c);
self.grid[line][Column(col)].push_zerowidth(c);
return;
}
@ -1521,8 +1529,10 @@ impl<T: EventListener> Handler for Term<T> {
fn decaln(&mut self) {
trace!("Decalnning");
let template = Cell { c: 'E', ..Cell::default() };
self.grid.region_mut(..).each(|c| c.reset(&template));
self.grid.region_mut(..).each(|cell| {
*cell = Cell::default();
cell.c = 'E';
});
}
#[inline]
@ -1553,7 +1563,8 @@ impl<T: EventListener> Handler for Term<T> {
#[inline]
fn insert_blank(&mut self, count: Column) {
let cursor = self.grid.cursor;
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
// Ensure inserting within terminal bounds
let count = min(count, self.cols() - cursor.point.col);
@ -1562,19 +1573,20 @@ impl<T: EventListener> Handler for Term<T> {
let destination = cursor.point.col + count;
let num_cells = (self.cols() - destination).0;
let line = &mut self.grid[cursor.point.line];
let line = cursor.point.line;
let row = &mut self.grid[line];
unsafe {
let src = line[source..].as_ptr();
let dst = line[destination..].as_mut_ptr();
let src = row[source..].as_ptr();
let dst = row[destination..].as_mut_ptr();
ptr::copy(src, dst, num_cells);
}
// Cells were just moved out toward the end of the line;
// fill in between source and dest with blanks.
for c in &mut line[source..destination] {
c.reset(&cursor.template);
for cell in &mut row[source..destination] {
*cell = bg.into();
}
}
@ -1802,7 +1814,7 @@ impl<T: EventListener> Handler for Term<T> {
#[inline]
fn erase_chars(&mut self, count: Column) {
let cursor = self.grid.cursor;
let cursor = &self.grid.cursor;
trace!("Erasing chars: count={}, col={}", count, cursor.point.col);
@ -1810,16 +1822,19 @@ impl<T: EventListener> Handler for Term<T> {
let end = min(start + count, self.cols());
// Cleared cells have current background color set.
let row = &mut self.grid[cursor.point.line];
for c in &mut row[start..end] {
c.reset(&cursor.template);
let bg = self.grid.cursor.template.bg;
let line = cursor.point.line;
let row = &mut self.grid[line];
for cell in &mut row[start..end] {
*cell = bg.into();
}
}
#[inline]
fn delete_chars(&mut self, count: Column) {
let cols = self.cols();
let cursor = self.grid.cursor;
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
// Ensure deleting within terminal bounds.
let count = min(count, cols);
@ -1828,20 +1843,21 @@ impl<T: EventListener> Handler for Term<T> {
let end = min(start + count, cols - 1);
let n = (cols - end).0;
let line = &mut self.grid[cursor.point.line];
let line = cursor.point.line;
let row = &mut self.grid[line];
unsafe {
let src = line[end..].as_ptr();
let dst = line[start..].as_mut_ptr();
let src = row[end..].as_ptr();
let dst = row[start..].as_mut_ptr();
ptr::copy(src, dst, n);
}
// Clear last `count` cells in line. If deleting 1 char, need to delete
// Clear last `count` cells in the row. If deleting 1 char, need to delete
// 1 cell.
let end = cols - count;
for c in &mut line[end..] {
c.reset(&cursor.template);
for cell in &mut row[end..] {
*cell = bg.into();
}
}
@ -1870,38 +1886,40 @@ impl<T: EventListener> Handler for Term<T> {
fn save_cursor_position(&mut self) {
trace!("Saving cursor position");
self.grid.saved_cursor = self.grid.cursor;
self.grid.saved_cursor = self.grid.cursor.clone();
}
#[inline]
fn restore_cursor_position(&mut self) {
trace!("Restoring cursor position");
self.grid.cursor = self.grid.saved_cursor;
self.grid.cursor = self.grid.saved_cursor.clone();
}
#[inline]
fn clear_line(&mut self, mode: ansi::LineClearMode) {
trace!("Clearing line: {:?}", mode);
let cursor = self.grid.cursor;
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
let point = cursor.point;
let row = &mut self.grid[point.line];
match mode {
ansi::LineClearMode::Right => {
let row = &mut self.grid[cursor.point.line];
for cell in &mut row[cursor.point.col..] {
cell.reset(&cursor.template);
for cell in &mut row[point.col..] {
*cell = bg.into();
}
},
ansi::LineClearMode::Left => {
let row = &mut self.grid[cursor.point.line];
for cell in &mut row[..=cursor.point.col] {
cell.reset(&cursor.template);
for cell in &mut row[..=point.col] {
*cell = bg.into();
}
},
ansi::LineClearMode::All => {
let row = &mut self.grid[cursor.point.line];
for cell in &mut row[..] {
cell.reset(&cursor.template);
*cell = bg.into();
}
},
}
@ -1986,7 +2004,7 @@ impl<T: EventListener> Handler for Term<T> {
#[inline]
fn clear_screen(&mut self, mode: ansi::ClearMode) {
trace!("Clearing screen: {:?}", mode);
let template = self.grid.cursor.template;
let bg = self.grid.cursor.template.bg;
let num_lines = self.screen_lines().0;
let cursor_buffer_line = num_lines - self.grid.cursor.point.line.0 - 1;
@ -1998,13 +2016,13 @@ impl<T: EventListener> Handler for Term<T> {
// If clearing more than one line.
if cursor.line > Line(1) {
// Fully clear all lines before the current line.
self.grid.region_mut(..cursor.line).each(|cell| cell.reset(&template));
self.grid.region_mut(..cursor.line).each(|cell| *cell = bg.into());
}
// Clear up to the current column in the current line.
let end = min(cursor.col + 1, self.cols());
for cell in &mut self.grid[cursor.line][..end] {
cell.reset(&template);
*cell = bg.into();
}
self.selection = self
@ -2015,11 +2033,11 @@ impl<T: EventListener> Handler for Term<T> {
ansi::ClearMode::Below => {
let cursor = self.grid.cursor.point;
for cell in &mut self.grid[cursor.line][cursor.col..] {
cell.reset(&template);
*cell = bg.into();
}
if cursor.line.0 < num_lines - 1 {
self.grid.region_mut((cursor.line + 1)..).each(|cell| cell.reset(&template));
self.grid.region_mut((cursor.line + 1)..).each(|cell| *cell = bg.into());
}
self.selection =
@ -2027,10 +2045,9 @@ impl<T: EventListener> Handler for Term<T> {
},
ansi::ClearMode::All => {
if self.mode.contains(TermMode::ALT_SCREEN) {
self.grid.region_mut(..).each(|c| c.reset(&template));
self.grid.region_mut(..).each(|cell| *cell = bg.into());
} else {
let template = Cell { bg: template.bg, ..Cell::default() };
self.grid.clear_viewport(template);
self.grid.clear_viewport();
}
self.selection = self.selection.take().filter(|s| !s.intersects_range(..num_lines));
@ -2069,8 +2086,8 @@ impl<T: EventListener> Handler for Term<T> {
self.colors = self.original_colors;
self.color_modified = [false; color::COUNT];
self.cursor_style = None;
self.grid.reset(Cell::default());
self.inactive_grid.reset(Cell::default());
self.grid.reset();
self.inactive_grid.reset();
self.scroll_region = Line(0)..self.screen_lines();
self.tabs = TabStops::new(self.cols());
self.title_stack = Vec::new();
@ -2492,7 +2509,7 @@ mod tests {
fn semantic_selection_works() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(5), 0, Cell::default());
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(5), 0);
for i in 0..5 {
for j in 0..2 {
grid[Line(j)][Column(i)].c = 'a';
@ -2540,7 +2557,7 @@ mod tests {
fn line_selection_works() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(5), 0, Cell::default());
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(5), 0);
for i in 0..5 {
grid[Line(0)][Column(i)].c = 'a';
}
@ -2561,7 +2578,7 @@ mod tests {
fn selecting_empty_line() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(3), 0, Cell::default());
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(3), 0);
for l in 0..3 {
if l != 1 {
for c in 0..3 {
@ -2585,9 +2602,7 @@ mod tests {
/// test this property with a T=Cell.
#[test]
fn grid_serde() {
let template = Cell::default();
let grid: Grid<Cell> = Grid::new(Line(24), Column(80), 0, template);
let grid: Grid<Cell> = Grid::new(Line(24), Column(80), 0);
let serialized = serde_json::to_string(&grid).expect("ser");
let deserialized = serde_json::from_str::<Grid<Cell>>(&serialized).expect("de");
@ -2611,7 +2626,7 @@ mod tests {
let mut term = Term::new(&MockConfig::default(), size, Mock);
// Add one line of scrollback.
term.grid.scroll_up(&(Line(0)..Line(1)), Line(1), Cell::default());
term.grid.scroll_up(&(Line(0)..Line(1)), Line(1));
// Clear the history.
term.clear_screen(ansi::ClearMode::Saved);

View File

@ -213,16 +213,19 @@ impl<T> Term<T> {
let mut iter = self.grid.iter_from(start);
let mut state = dfa.start_state();
let mut last_wrapped = false;
let mut regex_match = None;
let mut cell = *iter.cell();
let mut cell = iter.cell();
self.skip_fullwidth(&mut iter, &mut cell, direction);
let mut c = cell.c;
let mut point = iter.point();
loop {
// Convert char to array of bytes.
let mut buf = [0; 4];
let utf8_len = cell.c.encode_utf8(&mut buf).len();
let utf8_len = c.encode_utf8(&mut buf).len();
// Pass char to DFA as individual bytes.
for i in 0..utf8_len {
@ -251,42 +254,42 @@ impl<T> Term<T> {
}
// Advance grid cell iterator.
let mut new_cell = match next(&mut iter) {
Some(&cell) => cell,
let mut cell = match next(&mut iter) {
Some(cell) => cell,
None => {
// Wrap around to other end of the scrollback buffer.
let start = Point::new(last_line - point.line, last_col - point.col);
iter = self.grid.iter_from(start);
*iter.cell()
iter.cell()
},
};
self.skip_fullwidth(&mut iter, &mut new_cell, direction);
self.skip_fullwidth(&mut iter, &mut cell, direction);
let wrapped = cell.flags.contains(Flags::WRAPLINE);
c = cell.c;
let last_point = mem::replace(&mut point, iter.point());
let last_cell = mem::replace(&mut cell, new_cell);
// Handle linebreaks.
if (last_point.col == last_col
&& point.col == Column(0)
&& !last_cell.flags.contains(Flags::WRAPLINE))
|| (last_point.col == Column(0)
&& point.col == last_col
&& !cell.flags.contains(Flags::WRAPLINE))
if (last_point.col == last_col && point.col == Column(0) && !last_wrapped)
|| (last_point.col == Column(0) && point.col == last_col && !wrapped)
{
match regex_match {
Some(_) => break,
None => state = dfa.start_state(),
}
}
last_wrapped = wrapped;
}
regex_match
}
/// Advance a grid iterator over fullwidth characters.
fn skip_fullwidth(
fn skip_fullwidth<'a>(
&self,
iter: &mut GridIterator<'_, Cell>,
cell: &mut Cell,
iter: &'a mut GridIterator<'_, Cell>,
cell: &mut &'a Cell,
direction: Direction,
) {
match direction {
@ -295,13 +298,13 @@ impl<T> Term<T> {
},
Direction::Right if cell.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => {
if let Some(new_cell) = iter.next() {
*cell = *new_cell;
*cell = new_cell;
}
iter.next();
},
Direction::Left if cell.flags.contains(Flags::WIDE_CHAR_SPACER) => {
if let Some(new_cell) = iter.prev() {
*cell = *new_cell;
*cell = new_cell;
}
let prev = iter.point().sub_absolute(self, Boundary::Clamp, 1);

View File

@ -269,7 +269,7 @@ fn semantic<T: EventListener>(
// Expand semantically based on movement direction.
let expand_semantic = |point: Point<usize>| {
// Do not expand when currently on a semantic escape char.
let cell = term.grid()[point.line][point.col];
let cell = &term.grid()[point.line][point.col];
if term.semantic_escape_chars().contains(cell.c)
&& !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
{
@ -375,7 +375,7 @@ fn advance<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> Poin
/// Check if cell at point contains whitespace.
fn is_space<T>(term: &Term<T>, point: Point<usize>) -> bool {
let cell = term.grid()[point.line][point.col];
let cell = &term.grid()[point.line][point.col];
!cell.flags().intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
&& (cell.c == ' ' || cell.c == '\t')
}
@ -653,7 +653,7 @@ mod tests {
#[test]
fn scroll_semantic() {
let mut term = term();
term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), Default::default());
term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5));
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
@ -729,7 +729,7 @@ mod tests {
#[test]
fn scroll_word() {
let mut term = term();
term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), Default::default());
term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5));
let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));

View File

@ -111,14 +111,14 @@ fn ref_test(dir: &Path) {
// Truncate invisible lines from the grid.
let mut term_grid = terminal.grid().clone();
term_grid.initialize_all(Cell::default());
term_grid.initialize_all();
term_grid.truncate();
if grid != term_grid {
for i in 0..grid.total_lines() {
for j in 0..grid.cols().0 {
let cell = term_grid[i][Column(j)];
let original_cell = grid[i][Column(j)];
let cell = &term_grid[i][Column(j)];
let original_cell = &grid[i][Column(j)];
if original_cell != cell {
println!(
"[{i}][{j}] {original:?} => {now:?}",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"raw":{"inner":[{"inner":[{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]}],"occ":0},{"inner":[{"c":"A","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":"B","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":"C","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]}],"occ":3},{"inner":[{"c":"A","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":"B","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":"C","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0},"extra":[" "," "," "," "," "]}],"occ":3}],"zero":0,"visible_lines":2,"len":3},"cols":10,"lines":3,"display_offset":0,"max_scroll_limit":0,"url_highlight":null}
{"raw":{"inner":[{"inner":[{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}}],"occ":0},{"inner":[{"c":"A","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":"B","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":"C","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}}],"occ":3},{"inner":[{"c":"A","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":"B","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":"C","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}},{"c":" ","fg":{"Named":"Foreground"},"bg":{"Named":"Background"},"flags":{"bits":0}}],"occ":3}],"zero":0,"visible_lines":2,"len":3},"cols":10,"lines":3,"display_offset":0,"max_scroll_limit":0,"url_highlight":null}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
%  UL  ~/…/tests/ref/zerowidth  zerowidth-slice  [?2004hecho "( <0361>° <035c>ʖ <0361>°)"echo "( <0361>° <035c>ʖ <0361>°)"[?2004l
( ͡° ͜ʖ ͡°)
%  UL  ~/…/tests/ref/zerowidth  zerowidth-slice  [?2004hecho "( <0361>° <035c>ʖ <0361>°)"echo "( <0361>° <035c>ʖ <0361>°)"[?2004l
( ͡° ͜ʖ ͡°)
%  UL  ~/…/tests/ref/zerowidth  zerowidth-slice  [?2004h

View File

@ -1 +1 @@
{"history_size":1000}
{"history_size":0}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"width":939.0,"height":503.0,"cell_width":8.0,"cell_height":16.0,"padding_x":5.0,"padding_y":3.0,"cols":116,"screen_lines":31}
{"width":1259.0,"height":683.0,"cell_width":9.0,"cell_height":19.0,"padding_x":4.0,"padding_y":9.0,"screen_lines":35,"cols":139}

File diff suppressed because one or more lines are too long