feat(terminal): port page memory model (partial PageList)

First cut of the terminal core memory model from ZIG/src/terminal:
size, point, style, hyperlink, ref_counted_set, page and PageList.

Design (see docs/rewrite/ARCHITECTURE.md decisions 2-4):
- Cell/Row are 8-byte u64 wrappers with Zig's exact bit offsets.
- Page owns row/cell Vecs plus style set, grapheme and hyperlink maps;
  Page::layout reproduces Ghostty's byte-size formula on 64-bit Linux so
  scrollback-limit pruning matches.
- PageList keeps nodes in a generation-checked slab with a free list and
  list-owned tracked pins addressed by handles.

This is explicitly partial (docs/rewrite/notes/ghostty-terminal-
pagelist.md): 151/202 PageList tests ported; reflow rebuilds a snapshot
instead of Zig's incremental ReflowCursor; point<->pin conversion walks
from the list head (no active/viewport caches); RefCountedSet doesn't
reproduce probe-dependent ID reuse. A follow-up pass finishes parity.

Test Plan:
- cargo test -p ghostty-terminal (239 tests)
This commit is contained in:
ddidderr committed 2026-10-11 22:12:31 +02:00
1 parent 09cbe3bc97
commit 6cf16ed2e6
11 files changed
+8817 -3

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
//! Owned OSC 8 hyperlinks and per-page identifiers.
/// Nonzero page-local hyperlink identifier.
pub type Id = u16;
/// Mapping from a cell index to its hyperlink.
pub type Map = std::collections::HashMap<usize, Id>;
/// An explicit OSC 8 ID or an automatically generated screen-local ID.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum HyperlinkId {
/// Application-provided byte string.
Explicit(Vec<u8>),
/// Screen-generated identifier.
Implicit(u32),
}
/// A decoded hyperlink owning its ID and URI.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Hyperlink {
/// Hyperlink identity.
pub id: HyperlinkId,
/// URI bytes, which need not be UTF-8.
pub uri: Vec<u8>,
}
impl Hyperlink {
/// Duplicate the owned strings.
pub fn dupe(&self) -> Self {
self.clone()
}
/// Compare identity and URI.
pub fn eql(&self, other: &Self) -> bool {
self == other
}
/// Logical string allocation including 32-byte chunk rounding.
pub fn string_bytes(&self) -> usize {
self.uri.len().next_multiple_of(32)
+ match &self.id {
HyperlinkId::Explicit(v) => v.len().next_multiple_of(32),
HyperlinkId::Implicit(_) => 0,
}
}
}
/// Owned page entry; unlike Zig it needs no base address for decoding.
pub type PageEntry = Hyperlink;
/// Reference-counted set of page entries.
pub type Set = crate::ref_counted_set::RefCountedSet<PageEntry>;
+9 -1
View File
@@ -1 +1,9 @@
//! ghostty-terminal: see docs/rewrite/ARCHITECTURE.md for the module map.
//! Safe page-based terminal memory model.
#![cfg_attr(test, allow(clippy::unwrap_used))]
pub mod hyperlink;
pub mod page;
pub mod page_list;
pub mod point;
pub mod ref_counted_set;
pub mod size;
pub mod style;
+2216
View File
@@ -0,0 +1,2216 @@
//! Compact cells and rows, with bounded owned page storage.
use crate::{
hyperlink::{self, Hyperlink, HyperlinkId},
ref_counted_set::{AddError, Layout as SetLayout},
style::{self, Style},
};
use ghostty_vt::color::Rgb;
use std::collections::{HashMap, HashSet};
/// Default grapheme allocation request, in bytes.
pub const GRAPHEME_BYTES_DEFAULT: u32 = 1024;
/// Default string allocation request, in bytes.
pub const STRING_BYTES_DEFAULT: u32 = 2048;
/// Reference ABI size of a hyperlink set item.
pub const HYPERLINK_ITEM_SIZE: usize = 48;
/// Reference ABI size of a style set item.
pub const STYLE_ITEM_SIZE: usize = 36;
/// The standard page capacity (smaller grapheme region in tests, as in Zig).
pub const STD_CAPACITY: Capacity = Capacity {
cols: 215,
rows: 215,
styles: 128,
hyperlink_bytes: 192,
grapheme_bytes: if cfg!(test) { 512 } else { 8192 },
string_bytes: STRING_BYTES_DEFAULT,
};
/// ContentTag field of the packed grid representation.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ContentTag {
/// Single codepoint.
#[default]
Codepoint = 0,
/// First codepoint of a grapheme cluster.
CodepointGrapheme = 1,
/// Background-only palette color.
BgColorPalette = 2,
/// Background-only RGB color.
BgColorRgb = 3,
}
/// Wide field of the packed grid representation.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Wide {
/// Single grid cell.
#[default]
Narrow = 0,
/// Two-cell character.
Wide = 1,
/// Trailing half of a wide character.
SpacerTail = 2,
/// End-of-line spacer before a wrapped wide character.
SpacerHead = 3,
}
/// SemanticContent field of the packed grid representation.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SemanticContent {
/// Ordinary command output.
#[default]
Output = 0,
/// User input.
Input = 1,
/// Application prompt.
Prompt = 2,
}
/// SemanticPrompt field of the packed grid representation.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SemanticPrompt {
/// No prompt cells.
#[default]
None = 0,
/// Primary prompt row.
Prompt = 1,
/// Continuation prompt row.
PromptContinuation = 2,
}
/// Eight-byte terminal cell; all-zero bits describe an empty cell.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Cell(u64);
impl Cell {
/// Construct an unstyled single-codepoint cell.
pub fn init(cp: u32) -> Self {
let mut c = Self::default();
c.set_codepoint(cp);
c
}
fn bits(self, shift: u32, mask: u64) -> u64 {
(self.0 >> shift) & mask
}
fn set_bits(&mut self, shift: u32, mask: u64, value: u64) {
self.0 = (self.0 & !(mask << shift)) | ((value & mask) << shift);
}
/// Return the bit-packed representation.
pub const fn raw(self) -> u64 {
self.0
}
/// True when every bit is zero.
pub fn is_zero(self) -> bool {
self.0 == 0
}
/// Return the codepoint, or zero for background-only cells.
pub fn codepoint(self) -> u32 {
if (self.content_tag() as u8) < 2 {
self.bits(2, 0x1fffff) as u32
} else {
0
}
}
/// Set the codepoint payload without changing the content tag.
pub fn set_codepoint(&mut self, cp: u32) {
self.set_bits(2, 0xffffff, u64::from(cp & 0x1fffff));
}
/// Return the palette payload.
pub fn color_palette(self) -> u8 {
self.bits(2, 255) as u8
}
/// Set the palette payload without changing the content tag.
pub fn set_color_palette(&mut self, v: u8) {
self.set_bits(2, 0xffffff, u64::from(v));
}
/// Return the RGB payload.
pub fn color_rgb(self) -> Rgb {
Rgb {
r: self.bits(2, 255) as u8,
g: self.bits(10, 255) as u8,
b: self.bits(18, 255) as u8,
}
}
/// Set the RGB payload without changing the content tag.
pub fn set_color_rgb(&mut self, v: Rgb) {
self.set_bits(
2,
0xffffff,
u64::from(v.r) | (u64::from(v.g) << 8) | (u64::from(v.b) << 16),
);
}
/// True when the cell contains a nonzero text codepoint.
pub fn has_text(self) -> bool {
self.codepoint() != 0
}
/// True when the cell refers to extra grapheme codepoints.
pub fn has_grapheme(self) -> bool {
self.content_tag() == ContentTag::CodepointGrapheme
}
/// Width in grid cells, including one-cell spacers.
pub fn grid_width(self) -> u8 {
if self.wide() == Wide::Wide { 2 } else { 1 }
}
/// True when the cell has a nondefault style ID.
pub fn has_styling(self) -> bool {
self.style_id() != 0
}
/// True for a blank narrow textual cell; style does not affect this test.
pub fn is_empty(self) -> bool {
(self.content_tag() as u8) < 2 && !self.has_text() && self.wide() == Wide::Narrow
}
/// True if any cell has text.
pub fn has_text_any(cells: &[Self]) -> bool {
cells.iter().any(|c| c.has_text())
}
/// Read the content_tag field.
pub fn content_tag(self) -> ContentTag {
match self.bits(0, 3) {
0 => ContentTag::Codepoint,
1 => ContentTag::CodepointGrapheme,
2 => ContentTag::BgColorPalette,
_ => ContentTag::BgColorRgb,
}
}
/// Update the content_tag field.
pub fn set_content_tag(&mut self, v: ContentTag) {
self.set_bits(0, 3, v as u64);
}
/// Read the style_id field.
pub fn style_id(self) -> u16 {
self.bits(26, 65535) as u16
}
/// Update the style_id field.
pub fn set_style_id(&mut self, v: u16) {
self.set_bits(26, 65535, v as u64);
}
/// Read the wide field.
pub fn wide(self) -> Wide {
match self.bits(42, 3) {
0 => Wide::Narrow,
1 => Wide::Wide,
2 => Wide::SpacerTail,
_ => Wide::SpacerHead,
}
}
/// Update the wide field.
pub fn set_wide(&mut self, v: Wide) {
self.set_bits(42, 3, v as u64);
}
/// Read the protected field.
pub fn protected(self) -> bool {
self.bits(44, 1) != 0
}
/// Update the protected field.
pub fn set_protected(&mut self, v: bool) {
self.set_bits(44, 1, v as u64);
}
/// Read the hyperlink field.
pub fn hyperlink(self) -> bool {
self.bits(45, 1) != 0
}
/// Update the hyperlink field.
pub fn set_hyperlink(&mut self, v: bool) {
self.set_bits(45, 1, v as u64);
}
/// Read the semantic_content field.
pub fn semantic_content(self) -> SemanticContent {
match self.bits(46, 3) {
0 => SemanticContent::Output,
1 => SemanticContent::Input,
_ => SemanticContent::Prompt,
}
}
/// Update the semantic_content field.
pub fn set_semantic_content(&mut self, v: SemanticContent) {
self.set_bits(46, 3, v as u64);
}
}
/// Eight-byte row metadata. The low 32 bits hold its first cell index.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Row(u64);
impl Row {
/// First cell index in the page's row-major storage.
pub fn cells(self) -> usize {
(self.0 & 0xffffffff) as usize
}
/// Set the row's first cell index.
pub fn set_cells(&mut self, v: usize) {
self.0 = (self.0 & !0xffffffff) | (v as u64 & 0xffffffff);
}
/// True if cells can have managed style, hyperlink, or grapheme data.
pub fn managed_memory(self) -> bool {
self.styled() || self.hyperlink() || self.grapheme()
}
/// Prompt-row classification.
pub fn semantic_prompt(self) -> SemanticPrompt {
match (self.0 >> 37) & 3 {
1 => SemanticPrompt::Prompt,
2 => SemanticPrompt::PromptContinuation,
_ => SemanticPrompt::None,
}
}
/// Set prompt-row classification.
pub fn set_semantic_prompt(&mut self, v: SemanticPrompt) {
self.0 = (self.0 & !(3 << 37)) | ((v as u64) << 37);
}
/// Read the wrap flag.
pub fn wrap(self) -> bool {
self.0 & (1 << 32) != 0
}
/// Update the wrap flag.
pub fn set_wrap(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 32)) | (u64::from(v) << 32);
}
/// Read the wrap_continuation flag.
pub fn wrap_continuation(self) -> bool {
self.0 & (1 << 33) != 0
}
/// Update the wrap_continuation flag.
pub fn set_wrap_continuation(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 33)) | (u64::from(v) << 33);
}
/// Read the grapheme flag.
pub fn grapheme(self) -> bool {
self.0 & (1 << 34) != 0
}
/// Update the grapheme flag.
pub fn set_grapheme(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 34)) | (u64::from(v) << 34);
}
/// Read the styled flag.
pub fn styled(self) -> bool {
self.0 & (1 << 35) != 0
}
/// Update the styled flag.
pub fn set_styled(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 35)) | (u64::from(v) << 35);
}
/// Read the hyperlink flag.
pub fn hyperlink(self) -> bool {
self.0 & (1 << 36) != 0
}
/// Update the hyperlink flag.
pub fn set_hyperlink(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 36)) | (u64::from(v) << 36);
}
/// Read the kitty_virtual_placeholder flag.
pub fn kitty_virtual_placeholder(self) -> bool {
self.0 & (1 << 39) != 0
}
/// Update the kitty_virtual_placeholder flag.
pub fn set_kitty_virtual_placeholder(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 39)) | (u64::from(v) << 39);
}
/// Read the dirty flag.
pub fn dirty(self) -> bool {
self.0 & (1 << 40) != 0
}
/// Update the dirty flag.
pub fn set_dirty(&mut self, v: bool) {
self.0 = (self.0 & !(1 << 40)) | (u64::from(v) << 40);
}
}
/// Current logical page dimensions.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Size {
/// Columns currently addressable.
pub cols: u16,
/// Rows currently addressable.
pub rows: u16,
}
/// Fixed logical allocation limits for a page.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capacity {
/// Allocated columns.
pub cols: u16,
/// Allocated rows.
pub rows: u16,
/// Requested style hash-table slots.
pub styles: u16,
/// Requested hyperlink-set bytes, excluding strings and map.
pub hyperlink_bytes: u16,
/// Requested grapheme bytes.
pub grapheme_bytes: u32,
/// Requested string bytes.
pub string_bytes: u32,
}
impl Default for Capacity {
fn default() -> Self {
Self {
cols: 0,
rows: 0,
styles: 16,
hyperlink_bytes: 192,
grapheme_bytes: GRAPHEME_BYTES_DEFAULT,
string_bytes: STRING_BYTES_DEFAULT,
}
}
}
/// Capacity adjustment that retains the original total byte allocation.
#[derive(Debug, Default, Clone, Copy)]
pub struct Adjustment {
/// New column count; rows absorb the size change.
pub cols: Option<u16>,
}
impl Capacity {
fn available_bytes_for_grid(self) -> usize {
let l = Page::layout(self);
let mut end = l.total_size;
for (bytes, align) in [
(l.hyperlink_map_layout.total_size, 4),
(l.hyperlink_set_layout.total_size, 8),
(l.string_alloc_layout.total_size, 8),
(l.grapheme_map_layout.total_size, 8),
(l.grapheme_alloc_layout.total_size, 8),
(l.styles_layout.total_size, 8),
] {
end = (end - bytes) & !(align - 1);
}
end
}
/// Maximum columns that still allow one row without enlarging the page.
pub fn max_cols(self) -> Option<u16> {
let bytes = self.available_bytes_for_grid();
(bytes > 8).then(|| ((bytes - 8) / 8).min(65535) as u16)
}
/// Adjust columns, retaining the same page size by changing row capacity.
pub fn adjust(self, req: Adjustment) -> Result<Self, Error> {
let Some(cols) = req.cols else {
return Ok(self);
};
let rows = self.available_bytes_for_grid() / (8 + usize::from(cols) * 8);
if rows == 0 {
return Err(Error::OutOfMemory);
}
Ok(Self {
cols,
rows: rows.min(65535) as u16,
..self
})
}
}
/// Logical bitmap allocator layout, preserving the reference size formula.
#[derive(Debug, Default, Clone, Copy)]
pub struct AllocLayout {
/// Total bytes including the reference allocator's chunk multiplier.
pub total_size: usize,
/// Number of 64-bit chunk bitmaps.
pub bitmap_count: usize,
/// Start of allocation data.
pub chunks_start: usize,
}
impl AllocLayout {
fn new(cap: usize, chunk: usize) -> Self {
let aligned = cap.next_multiple_of(chunk);
let bitmap_count = (aligned / chunk).div_ceil(64);
Self {
total_size: bitmap_count * 8 + aligned * chunk,
bitmap_count,
chunks_start: bitmap_count * 8,
}
}
}
/// Logical offset hash-map layout.
#[derive(Debug, Default, Clone, Copy)]
pub struct MapLayout {
/// Total bytes including the 16-byte header.
pub total_size: usize,
/// Maximum entries (no extra load-factor reduction).
pub capacity: usize,
}
impl MapLayout {
fn new(capacity: usize, value_size: usize, value_align: usize) -> Self {
let keys_start = (16 + capacity).next_multiple_of(4);
let vals_start = (keys_start + capacity * 4).next_multiple_of(value_align);
Self {
total_size: (vals_start + capacity * value_size).next_multiple_of(value_align.max(4)),
capacity,
}
}
}
/// Complete logical page layout for the 64-bit Linux reference ABI.
#[derive(Debug, Clone, Copy)]
pub struct Layout {
/// Page-aligned byte size.
pub total_size: usize,
/// Row array offset.
pub rows_start: usize,
/// Row array bytes.
pub rows_size: usize,
/// Cell array offset.
pub cells_start: usize,
/// Cell array bytes.
pub cells_size: usize,
/// Style set offset.
pub styles_start: usize,
/// Style set layout.
pub styles_layout: SetLayout,
/// Grapheme allocator offset.
pub grapheme_alloc_start: usize,
/// Grapheme allocator layout.
pub grapheme_alloc_layout: AllocLayout,
/// Grapheme map offset.
pub grapheme_map_start: usize,
/// Grapheme map layout.
pub grapheme_map_layout: MapLayout,
/// String allocator offset.
pub string_alloc_start: usize,
/// String allocator layout.
pub string_alloc_layout: AllocLayout,
/// Hyperlink set offset.
pub hyperlink_set_start: usize,
/// Hyperlink set layout.
pub hyperlink_set_layout: SetLayout,
/// Hyperlink map offset.
pub hyperlink_map_start: usize,
/// Hyperlink map layout.
pub hyperlink_map_layout: MapLayout,
/// Requested capacity.
pub capacity: Capacity,
}
/// Recoverable page operation errors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// Allocation or grid capacity exhausted.
OutOfMemory,
/// Cell or row is outside the logical page.
InvalidCoordinate,
/// A grid dimension is zero.
InvalidDimensions,
/// Style set needs more space.
StyleSetOutOfMemory,
/// Style IDs need compaction.
StyleSetNeedsRehash,
/// No contiguous grapheme allocation fits.
GraphemeAllocOutOfMemory,
/// Grapheme map capacity exhausted.
GraphemeMapOutOfMemory,
/// Hyperlink map capacity exhausted.
HyperlinkMapOutOfMemory,
/// Hyperlink set capacity exhausted.
SetOutOfMemory,
/// Hyperlink IDs need compaction.
SetNeedsRehash,
/// No contiguous string allocation fits.
StringsOutOfMemory,
/// Missing style or hyperlink ID.
InvalidId,
/// Reference count overflow or underflow.
InvalidRefCount,
/// Capacity cannot grow further.
OutOfSpace,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for Error {}
impl From<AddError> for Error {
fn from(e: AddError) -> Self {
match e {
AddError::OutOfMemory => Self::StyleSetOutOfMemory,
AddError::NeedsRehash => Self::StyleSetNeedsRehash,
AddError::InvalidId => Self::InvalidId,
AddError::InvalidRefCount => Self::InvalidRefCount,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Allocation {
start: usize,
len: usize,
}
#[derive(Debug, Clone)]
struct ChunkAlloc {
used: Vec<bool>,
chunk: usize,
}
impl ChunkAlloc {
fn new(layout: AllocLayout, chunk: usize) -> Result<Self, Error> {
let mut used = Vec::new();
let count = layout.bitmap_count * 64;
used.try_reserve_exact(count)
.map_err(|_| Error::OutOfMemory)?;
used.resize(count, false);
Ok(Self { used, chunk })
}
fn alloc(&mut self, bytes: usize) -> Option<Allocation> {
let n = bytes.div_ceil(self.chunk);
if n == 0 {
return Some(Allocation { start: 0, len: 0 });
}
let mut run = 0;
for i in 0..self.used.len() {
if self.used[i] {
run = 0;
} else {
run += 1;
}
if run == n {
let start = i + 1 - n;
self.used[start..=i].fill(true);
return Some(Allocation { start, len: n });
}
}
None
}
fn free(&mut self, a: Allocation) {
self.used[a.start..a.start + a.len].fill(false);
}
fn reset(&mut self) {
self.used.fill(false);
}
}
/// Page-owned rows, cells, and bounded auxiliary storage.
#[derive(Debug, Clone)]
pub struct Page {
/// Logical dimensions; no greater than capacity.
pub size: Size,
/// Fixed logical allocation limits.
pub capacity: Capacity,
/// A full-page operation dirtied every row.
pub dirty: bool,
/// Row metadata in screen order.
pub rows: Vec<Row>,
/// Cells in row-major capacity order.
pub cells: Vec<Cell>,
/// Shared nondefault cell styles.
pub styles: style::Set,
/// Shared hyperlink values.
pub hyperlink_set: hyperlink::Set,
grapheme_map: HashMap<usize, Vec<u32>>,
grapheme_allocations: HashMap<usize, Allocation>,
grapheme_alloc: ChunkAlloc,
hyperlink_map: hyperlink::Map,
string_alloc: ChunkAlloc,
link_allocations: HashMap<u16, Vec<Allocation>>,
pause_integrity_checks: usize,
}
/// A cell with its page-independent managed values, used when copying pages.
#[derive(Debug, Clone)]
pub(crate) struct OwnedCell {
pub cell: Cell,
pub style: Option<Style>,
pub grapheme: Option<Vec<u32>>,
pub hyperlink: Option<Hyperlink>,
}
impl Page {
/// Compute the reference Zig layout without allocating memory.
pub fn layout(cap: Capacity) -> Layout {
let rows_start = 0;
let rows_size = usize::from(cap.rows) * 8;
let cells_start = rows_size;
let cells_size = usize::from(cap.rows) * usize::from(cap.cols) * 8;
let styles_start = (cells_start + cells_size).next_multiple_of(8);
let styles_layout = SetLayout::init(usize::from(cap.styles), STYLE_ITEM_SIZE, 4);
let grapheme_alloc_start = (styles_start + styles_layout.total_size).next_multiple_of(8);
let grapheme_alloc_layout = AllocLayout::new(cap.grapheme_bytes as usize, 16);
let grapheme_map_start =
(grapheme_alloc_start + grapheme_alloc_layout.total_size).next_multiple_of(8);
let gc = if cap.grapheme_bytes == 0 {
0
} else {
(cap.grapheme_bytes as usize)
.div_ceil(16)
.next_power_of_two()
};
let grapheme_map_layout = MapLayout::new(gc, 16, 8);
let string_alloc_start =
(grapheme_map_start + grapheme_map_layout.total_size).next_multiple_of(8);
let string_alloc_layout = AllocLayout::new(cap.string_bytes as usize, 32);
let hyperlink_set_start =
(string_alloc_start + string_alloc_layout.total_size).next_multiple_of(8);
let hc = usize::from(cap.hyperlink_bytes) / HYPERLINK_ITEM_SIZE;
let hyperlink_set_layout = SetLayout::init(hc, HYPERLINK_ITEM_SIZE, 8);
let hyperlink_map_start =
(hyperlink_set_start + hyperlink_set_layout.total_size).next_multiple_of(4);
let hyperlink_map_layout = MapLayout::new(
if hc == 0 {
0
} else {
(hc * 16).next_power_of_two()
},
2,
2,
);
let total_size =
(hyperlink_map_start + hyperlink_map_layout.total_size).next_multiple_of(4096);
Layout {
total_size,
rows_start,
rows_size,
cells_start,
cells_size,
styles_start,
styles_layout,
grapheme_alloc_start,
grapheme_alloc_layout,
grapheme_map_start,
grapheme_map_layout,
string_alloc_start,
string_alloc_layout,
hyperlink_set_start,
hyperlink_set_layout,
hyperlink_map_start,
hyperlink_map_layout,
capacity: cap,
}
}
/// Allocate zeroed rows and cells. Initial logical size is the full capacity.
pub fn init(cap: Capacity) -> Result<Self, Error> {
if cap.cols == 0 || cap.rows == 0 {
return Err(Error::InvalidDimensions);
}
let l = Self::layout(cap);
if l.total_size > u32::MAX as usize {
return Err(Error::OutOfSpace);
}
let mut rows = Vec::new();
rows.try_reserve_exact(usize::from(cap.rows))
.map_err(|_| Error::OutOfMemory)?;
for y in 0..cap.rows {
let mut row = Row::default();
row.set_cells(usize::from(y) * usize::from(cap.cols));
rows.push(row);
}
let mut cells = Vec::new();
let count = usize::from(cap.cols) * usize::from(cap.rows);
cells
.try_reserve_exact(count)
.map_err(|_| Error::OutOfMemory)?;
cells.resize(count, Cell::default());
Ok(Self {
size: Size {
rows: cap.rows,
cols: cap.cols,
},
capacity: cap,
dirty: false,
rows,
cells,
styles: style::Set::init(usize::from(cap.styles))?,
hyperlink_set: hyperlink::Set::init(usize::from(cap.hyperlink_bytes) / 48)?,
grapheme_map: HashMap::new(),
grapheme_allocations: HashMap::new(),
grapheme_alloc: ChunkAlloc::new(l.grapheme_alloc_layout, 16)?,
hyperlink_map: HashMap::new(),
string_alloc: ChunkAlloc::new(l.string_alloc_layout, 32)?,
link_allocations: HashMap::new(),
pause_integrity_checks: 0,
})
}
/// Reset contents, retaining grid and allocator backing storage.
pub fn reinit(&mut self) -> Result<(), Error> {
self.cells.fill(Cell::default());
for (y, r) in self.rows.iter_mut().enumerate() {
*r = Row::default();
r.set_cells(y * usize::from(self.capacity.cols));
}
self.size = Size {
cols: self.capacity.cols,
rows: self.capacity.rows,
};
self.dirty = false;
self.grapheme_map.clear();
self.grapheme_allocations.clear();
self.grapheme_alloc.reset();
self.hyperlink_map.clear();
self.string_alloc.reset();
self.link_allocations.clear();
self.styles = style::Set::init(usize::from(self.capacity.styles))?;
self.hyperlink_set = hyperlink::Set::init(usize::from(self.capacity.hyperlink_bytes) / 48)?;
Ok(())
}
/// Return row metadata by logical row number.
pub fn get_row(&self, y: usize) -> Option<&Row> {
self.rows.get(y).filter(|_| y < usize::from(self.size.rows))
}
/// Return mutable row metadata by logical row number.
pub fn get_row_mut(&mut self, y: usize) -> Option<&mut Row> {
self.rows
.get_mut(y)
.filter(|_| y < usize::from(self.size.rows))
}
/// Return the visible cells of a logical row in O(1).
pub fn get_cells(&self, y: usize) -> Option<&[Cell]> {
let start = self.get_row(y)?.cells();
self.cells.get(start..start + usize::from(self.size.cols))
}
/// Return mutable visible cells of a logical row in O(1).
pub fn get_cells_mut(&mut self, y: usize) -> Option<&mut [Cell]> {
let start = self.get_row(y)?.cells();
self.cells
.get_mut(start..start + usize::from(self.size.cols))
}
/// Return a cell index for a logical coordinate.
pub fn cell_index(&self, x: usize, y: usize) -> Result<usize, Error> {
if x >= usize::from(self.size.cols) {
return Err(Error::InvalidCoordinate);
}
Ok(self.get_row(y).ok_or(Error::InvalidCoordinate)?.cells() + x)
}
/// Return both metadata and cell at a logical coordinate.
pub fn get_row_and_cell(&self, x: usize, y: usize) -> Option<(&Row, &Cell)> {
Some((self.get_row(y)?, &self.cells[self.cell_index(x, y).ok()?]))
}
/// Resolve style without a map lookup for the default style.
pub fn style(&self, cell: Cell) -> Style {
if cell.style_id() == 0 {
Style::default()
} else {
self.styles
.get(cell.style_id())
.copied()
.unwrap_or_default()
}
}
/// Assign and retain styling, releasing the cell's previous style.
pub fn set_style(&mut self, x: usize, y: usize, value: Style) -> Result<(), Error> {
let i = self.cell_index(x, y)?;
let id = if value.is_default() {
0
} else {
self.styles.add(value)?
};
let old = self.cells[i].style_id();
if old != 0 {
self.styles.release(old)?;
}
self.cells[i].set_style_id(id);
if id != 0 {
self.rows[y].set_styled(true);
}
self.rows[y].set_dirty(true);
Ok(())
}
/// Return additional codepoints, excluding the first stored on the cell.
pub fn lookup_grapheme(&self, index: usize) -> Option<&[u32]> {
self.grapheme_map.get(&index).map(Vec::as_slice)
}
/// Number of cells with grapheme data.
pub fn grapheme_count(&self) -> usize {
self.grapheme_map.len()
}
/// Maximum number of grapheme map entries.
pub fn grapheme_capacity(&self) -> usize {
Self::layout(self.capacity).grapheme_map_layout.capacity
}
/// Attach additional codepoints to a textual cell.
pub fn set_graphemes(&mut self, x: usize, y: usize, cps: &[u32]) -> Result<(), Error> {
let i = self.cell_index(x, y)?;
if self.cells[i].codepoint() == 0 || cps.is_empty() || self.cells[i].has_grapheme() {
return Err(Error::InvalidCoordinate);
}
if self.grapheme_count() >= self.grapheme_capacity() {
return Err(Error::GraphemeMapOutOfMemory);
}
self.grapheme_map
.try_reserve(1)
.map_err(|_| Error::OutOfMemory)?;
self.grapheme_allocations
.try_reserve(1)
.map_err(|_| Error::OutOfMemory)?;
let mut data = Vec::new();
data.try_reserve_exact(cps.len())
.map_err(|_| Error::OutOfMemory)?;
data.extend_from_slice(cps);
let a = self
.grapheme_alloc
.alloc(cps.len() * 4)
.ok_or(Error::GraphemeAllocOutOfMemory)?;
self.grapheme_map.insert(i, data);
self.grapheme_allocations.insert(i, a);
self.cells[i].set_content_tag(ContentTag::CodepointGrapheme);
self.rows[y].set_grapheme(true);
self.rows[y].set_dirty(true);
Ok(())
}
/// Append one additional codepoint, retaining allocation chunk semantics.
pub fn append_grapheme(&mut self, x: usize, y: usize, cp: u32) -> Result<(), Error> {
let i = self.cell_index(x, y)?;
if !self.cells[i].has_grapheme() {
return self.set_graphemes(x, y, &[cp]);
}
let data = self
.grapheme_map
.get_mut(&i)
.ok_or(Error::InvalidCoordinate)?;
data.try_reserve(1).map_err(|_| Error::OutOfMemory)?;
if data.len().is_multiple_of(4) {
let a = self
.grapheme_alloc
.alloc((data.len() + 1) * 4)
.ok_or(Error::GraphemeAllocOutOfMemory)?;
if let Some(old) = self.grapheme_allocations.insert(i, a) {
self.grapheme_alloc.free(old);
}
}
data.push(cp);
self.rows[y].set_dirty(true);
Ok(())
}
/// Release a cell's additional codepoints.
pub fn clear_grapheme(&mut self, index: usize) {
if self.grapheme_map.remove(&index).is_some()
&& let Some(a) = self.grapheme_allocations.remove(&index)
{
self.grapheme_alloc.free(a);
}
if let Some(c) = self.cells.get_mut(index)
&& c.has_grapheme()
{
c.set_content_tag(ContentTag::Codepoint);
}
}
/// Move grapheme ownership between cell indices.
pub fn move_grapheme(&mut self, src: usize, dst: usize) -> Result<(), Error> {
if src >= self.cells.len() || dst >= self.cells.len() {
return Err(Error::InvalidCoordinate);
}
if src == dst {
return Ok(());
}
self.clear_grapheme(dst);
if let Some(data) = self.grapheme_map.remove(&src) {
self.grapheme_map.insert(dst, data);
if let Some(a) = self.grapheme_allocations.remove(&src) {
self.grapheme_allocations.insert(dst, a);
}
self.cells[src].set_content_tag(ContentTag::Codepoint);
self.cells[dst].set_content_tag(ContentTag::CodepointGrapheme);
}
Ok(())
}
/// Look up a cell's page-local hyperlink ID.
pub fn lookup_hyperlink(&self, index: usize) -> Option<u16> {
self.hyperlink_map.get(&index).copied()
}
/// Number of hyperlink cells.
pub fn hyperlink_count(&self) -> usize {
self.hyperlink_map.len()
}
/// Maximum hyperlink-map entries.
pub fn hyperlink_capacity(&self) -> usize {
Self::layout(self.capacity).hyperlink_map_layout.capacity
}
/// Allocate strings and retain a page-local hyperlink.
pub fn insert_hyperlink(&mut self, link: Hyperlink) -> Result<u16, Error> {
let mut allocations = Vec::with_capacity(2);
let uri = self
.string_alloc
.alloc(link.uri.len())
.ok_or(Error::StringsOutOfMemory)?;
allocations.push(uri);
if let HyperlinkId::Explicit(v) = &link.id {
if let Some(a) = self.string_alloc.alloc(v.len()) {
allocations.push(a);
} else {
self.string_alloc.free(uri);
return Err(Error::StringsOutOfMemory);
}
}
let result = self.hyperlink_set.add(link);
let id = match result {
Ok(id) => id,
Err(e) => {
for a in allocations {
self.string_alloc.free(a);
}
return Err(match e {
AddError::NeedsRehash => Error::SetNeedsRehash,
_ => Error::SetOutOfMemory,
});
}
};
// Trimmed set entries no longer own their retained string chunks.
let dead: Vec<_> = self
.link_allocations
.keys()
.copied()
.filter(|id| !self.hyperlink_set.contains_slot(*id))
.collect();
for id in dead {
if let Some(v) = self.link_allocations.remove(&id) {
for a in v {
self.string_alloc.free(a);
}
}
}
if self.hyperlink_set.ref_count(id) > 1 {
for a in allocations {
self.string_alloc.free(a);
}
} else if let Some(v) = self.link_allocations.insert(id, allocations) {
for a in v {
self.string_alloc.free(a);
}
}
Ok(id)
}
/// Set a hyperlink, consuming an existing retained reference (no increment).
pub fn set_hyperlink(&mut self, x: usize, y: usize, id: u16) -> Result<(), Error> {
let i = self.cell_index(x, y)?;
if self.hyperlink_set.get(id).is_none() {
return Err(Error::InvalidId);
}
if !self.hyperlink_map.contains_key(&i)
&& self.hyperlink_count() >= self.hyperlink_capacity()
{
return Err(Error::HyperlinkMapOutOfMemory);
}
self.hyperlink_map
.try_reserve(1)
.map_err(|_| Error::OutOfMemory)?;
self.clear_hyperlink(i)?;
self.hyperlink_map.insert(i, id);
self.cells[i].set_hyperlink(true);
self.rows[y].set_hyperlink(true);
Ok(())
}
/// Release a cell's hyperlink reference.
pub fn clear_hyperlink(&mut self, i: usize) -> Result<(), Error> {
if let Some(id) = self.hyperlink_map.remove(&i) {
self.hyperlink_set.release(id)?;
}
if let Some(c) = self.cells.get_mut(i) {
c.set_hyperlink(false);
}
Ok(())
}
/// Recompute the conservative row grapheme flag.
pub fn update_row_grapheme_flag(&mut self, y: usize) {
if let Some(c) = self.get_cells(y) {
let v = c.iter().any(|c| c.has_grapheme());
self.rows[y].set_grapheme(v);
}
}
/// Recompute the conservative row style flag.
pub fn update_row_styled_flag(&mut self, y: usize) {
if let Some(c) = self.get_cells(y) {
let v = c.iter().any(|c| c.has_styling());
self.rows[y].set_styled(v);
}
}
/// Recompute the conservative row hyperlink flag.
pub fn update_row_hyperlink_flag(&mut self, y: usize) {
if let Some(c) = self.get_cells(y) {
let v = c.iter().any(|c| c.hyperlink());
self.rows[y].set_hyperlink(v);
}
}
/// True if the page or any visible row is dirty.
pub fn is_dirty(&self) -> bool {
self.dirty
|| self.rows[..usize::from(self.size.rows)]
.iter()
.any(|r| r.dirty())
}
/// Clear a half-open column range, optionally preserving protected cells.
pub fn clear_cells(
&mut self,
y: usize,
start: usize,
end: usize,
protect: bool,
) -> Result<(), Error> {
if start > end || end > usize::from(self.size.cols) || y >= usize::from(self.size.rows) {
return Err(Error::InvalidCoordinate);
}
let base = self.rows[y].cells();
for i in base + start..base + end {
let c = self.cells[i];
if protect && c.protected() {
continue;
}
if c.has_styling() {
self.styles.release(c.style_id())?;
}
if c.has_grapheme() {
self.clear_grapheme(i);
}
if c.hyperlink() {
self.clear_hyperlink(i)?;
}
self.cells[i] = Cell::default();
}
self.rows[y].set_dirty(true);
Ok(())
}
/// Move a half-open cell range, clearing overwritten values and the source.
pub fn move_cells(
&mut self,
src_y: usize,
src_x: usize,
dst_y: usize,
dst_x: usize,
len: usize,
) -> Result<(), Error> {
if src_x + len > usize::from(self.size.cols) || dst_x + len > usize::from(self.size.cols) {
return Err(Error::InvalidCoordinate);
}
let src = self.cell_index(src_x, src_y)?;
let dst = self.cell_index(dst_x, dst_y)?;
if src == dst {
return Ok(());
}
// Move in memmove order; auxiliary maps follow the cell indices.
for n in 0..len {
let n = if dst > src { len - 1 - n } else { n };
let si = src + n;
let di = dst + n;
self.clear_cells(dst_y, dst_x + n, dst_x + n + 1, false)?;
let c = self.cells[si];
self.cells[di] = c;
self.cells[si] = Cell::default();
if c.has_grapheme() {
if let Some(v) = self.grapheme_map.remove(&si) {
self.grapheme_map.insert(di, v);
}
if let Some(a) = self.grapheme_allocations.remove(&si) {
self.grapheme_allocations.insert(di, a);
}
}
if c.hyperlink()
&& let Some(v) = self.hyperlink_map.remove(&si)
{
self.hyperlink_map.insert(di, v);
}
}
for y in [src_y, dst_y] {
self.update_row_grapheme_flag(y);
self.update_row_styled_flag(y);
self.update_row_hyperlink_flag(y);
self.rows[y].set_dirty(true);
}
Ok(())
}
/// Swap two cells and their managed values without changing reference counts.
pub fn swap_cells(&mut self, ax: usize, ay: usize, bx: usize, by: usize) -> Result<(), Error> {
let a = self.cell_index(ax, ay)?;
let b = self.cell_index(bx, by)?;
if a == b {
return Ok(());
}
self.cells.swap(a, b);
swap_map(&mut self.grapheme_map, a, b);
swap_map(&mut self.grapheme_allocations, a, b);
swap_map(&mut self.hyperlink_map, a, b);
for y in [ay, by] {
self.update_row_grapheme_flag(y);
self.update_row_styled_flag(y);
self.update_row_hyperlink_flag(y);
self.rows[y].set_dirty(true);
}
Ok(())
}
pub(crate) fn export_cell(&self, i: usize) -> OwnedCell {
let cell = self.cells[i];
OwnedCell {
cell,
style: if cell.has_styling() {
self.styles.get(cell.style_id()).copied()
} else {
None
},
grapheme: if cell.has_grapheme() {
self.grapheme_map.get(&i).cloned()
} else {
None
},
hyperlink: if cell.hyperlink() {
self.lookup_hyperlink(i)
.and_then(|id| self.hyperlink_set.get(id))
.cloned()
} else {
None
},
}
}
pub(crate) fn import_cell(&mut self, x: usize, y: usize, v: &OwnedCell) -> Result<(), Error> {
let i = self.cell_index(x, y)?;
self.clear_cells(y, x, x + 1, false)?;
let mut c = v.cell;
c.set_style_id(0);
c.set_hyperlink(false);
if c.has_grapheme() {
c.set_content_tag(ContentTag::Codepoint);
}
self.cells[i] = c;
if let Some(s) = v.style {
self.set_style(x, y, s)?;
}
if let Some(g) = &v.grapheme {
self.set_graphemes(x, y, g)?;
}
if let Some(h) = &v.hyperlink {
let id = if let Some(id) = self.hyperlink_set.lookup(h) {
self.hyperlink_set.r#use(id)?;
id
} else {
self.insert_hyperlink(h.clone())?
};
if let Err(e) = self.set_hyperlink(x, y, id) {
self.hyperlink_set.release(id)?;
return Err(e);
}
}
Ok(())
}
/// Clone rows from another page, truncating or padding columns as needed.
pub fn clone_from(&mut self, src: &Self, y_start: usize, y_end: usize) -> Result<(), Error> {
if y_start > y_end
|| y_end > usize::from(src.size.rows)
|| y_end - y_start > usize::from(self.capacity.rows)
{
return Err(Error::InvalidCoordinate);
}
for y in 0..y_end - y_start {
self.clone_row_from(src, y_start + y, y)?;
}
Ok(())
}
/// Clone one complete row to a destination row.
pub fn clone_row_from(&mut self, src: &Self, src_y: usize, dst_y: usize) -> Result<(), Error> {
let cols = usize::from(self.size.cols.min(src.size.cols));
self.clear_cells(dst_y, 0, usize::from(self.size.cols), false)?;
self.clone_partial_row_from(src, src_y, dst_y, 0, 0, cols)?;
let start = self.rows[dst_y].cells();
self.rows[dst_y] = *src.get_row(src_y).ok_or(Error::InvalidCoordinate)?;
self.rows[dst_y].set_cells(start);
if self.size.cols > src.size.cols {
let i = self.cell_index(usize::from(src.size.cols - 1), dst_y)?;
if self.cells[i].wide() == Wide::SpacerHead {
self.cells[i].set_wide(Wide::Narrow);
}
}
Ok(())
}
/// Clone a column range, preserving untouched destination cells.
pub fn clone_partial_row_from(
&mut self,
src: &Self,
src_y: usize,
dst_y: usize,
src_x: usize,
dst_x: usize,
len: usize,
) -> Result<(), Error> {
if src_x
.checked_add(len)
.is_none_or(|end| end > usize::from(src.size.cols))
|| dst_x
.checked_add(len)
.is_none_or(|end| end > usize::from(self.size.cols))
{
return Err(Error::InvalidCoordinate);
}
let source_row = *src.get_row(src_y).ok_or(Error::InvalidCoordinate)?;
let original_row = *self.get_row(dst_y).ok_or(Error::InvalidCoordinate)?;
self.clear_cells(dst_y, dst_x, dst_x + len, false)?;
if source_row.managed_memory() {
for n in 0..len {
let i = src.cell_index(src_x + n, src_y)?;
self.import_cell(dst_x + n, dst_y, &src.export_cell(i))?;
}
} else {
let from = source_row.cells() + src_x;
let to = original_row.cells() + dst_x;
self.cells[to..to + len].copy_from_slice(&src.cells[from..from + len]);
}
let managed = self.rows[dst_y];
let mut metadata = source_row;
metadata.set_cells(original_row.cells());
if len < usize::from(self.size.cols) {
metadata.set_wrap(original_row.wrap());
metadata.set_wrap_continuation(original_row.wrap_continuation());
metadata.set_grapheme(managed.grapheme());
metadata.set_styled(managed.styled());
metadata.set_hyperlink(managed.hyperlink());
metadata.set_dirty(source_row.dirty() || original_row.dirty());
}
self.rows[dst_y] = metadata;
Ok(())
}
/// Clone a range within the same page, including overlapping ranges.
pub fn clone_cells_within(
&mut self,
src_y: usize,
dst_y: usize,
src_x: usize,
dst_x: usize,
len: usize,
) -> Result<(), Error> {
if src_x + len > usize::from(self.size.cols) || dst_x + len > usize::from(self.size.cols) {
return Err(Error::InvalidCoordinate);
}
let src = self.cell_index(src_x, src_y)?;
self.cell_index(dst_x, dst_y)?;
let data: Vec<_> = (src..src + len).map(|i| self.export_cell(i)).collect();
for (n, v) in data.iter().enumerate() {
self.import_cell(dst_x + n, dst_y, v)?;
}
Ok(())
}
/// Minimum capacity that can clone the half-open row range.
pub fn exact_row_capacity(&self, start: usize, end: usize) -> Result<Capacity, Error> {
if start >= end || end > usize::from(self.size.rows) {
return Err(Error::InvalidCoordinate);
}
let mut styles = HashSet::new();
let mut links = HashSet::new();
let mut gb = 0;
let mut sb = 0;
let mut hc: usize = 0;
for y in start..end {
let base = self.rows[y].cells();
for x in 0..usize::from(self.size.cols) {
let i = base + x;
let c = self.cells[i];
if c.has_styling() {
styles.insert(c.style_id());
}
if let Some(g) = self.lookup_grapheme(i) {
gb += (g.len() * 4).next_multiple_of(16);
}
if let Some(id) = self.lookup_hyperlink(i) {
hc += 1;
if links.insert(id)
&& let Some(h) = self.hyperlink_set.get(id)
{
sb += h.string_bytes();
}
}
}
}
let sc = style::Set::capacity_for_count(styles.len());
let lc = hyperlink::Set::capacity_for_count(links.len()).max(hc.div_ceil(16));
Ok(Capacity {
cols: self.size.cols,
rows: (end - start) as u16,
styles: u16::try_from(sc).map_err(|_| Error::OutOfSpace)?,
hyperlink_bytes: u16::try_from(lc * 48).map_err(|_| Error::OutOfSpace)?,
grapheme_bytes: u32::try_from(gb).map_err(|_| Error::OutOfSpace)?,
string_bytes: u32::try_from(sb).map_err(|_| Error::OutOfSpace)?,
})
}
/// Nestable suspension of debug integrity checks.
pub fn pause_integrity_checks(&mut self, pause: bool) {
if pause {
self.pause_integrity_checks += 1;
} else {
self.pause_integrity_checks = self.pause_integrity_checks.saturating_sub(1);
}
}
}
/// Structural invariant failures reported by debug/test verification.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegrityError {
/// ZeroRowCount invariant violation.
ZeroRowCount,
/// ZeroColCount invariant violation.
ZeroColCount,
/// UnmarkedGraphemeRow invariant violation.
UnmarkedGraphemeRow,
/// MissingGraphemeData invariant violation.
MissingGraphemeData,
/// InvalidGraphemeCount invariant violation.
InvalidGraphemeCount,
/// UnmarkedGraphemeCell invariant violation.
UnmarkedGraphemeCell,
/// MissingStyle invariant violation.
MissingStyle,
/// UnmarkedStyleRow invariant violation.
UnmarkedStyleRow,
/// MismatchedStyleRef invariant violation.
MismatchedStyleRef,
/// InvalidStyleCount invariant violation.
InvalidStyleCount,
/// MissingHyperlinkData invariant violation.
MissingHyperlinkData,
/// MismatchedHyperlinkRef invariant violation.
MismatchedHyperlinkRef,
/// UnmarkedHyperlinkCell invariant violation.
UnmarkedHyperlinkCell,
/// UnmarkedHyperlinkRow invariant violation.
UnmarkedHyperlinkRow,
/// InvalidSpacerTailLocation invariant violation.
InvalidSpacerTailLocation,
/// InvalidSpacerHeadLocation invariant violation.
InvalidSpacerHeadLocation,
/// UnwrappedSpacerHead invariant violation.
UnwrappedSpacerHead,
/// InvalidDimensions invariant violation.
InvalidDimensions,
}
#[cfg(any(debug_assertions, test))]
impl Page {
/// Verify grid, metadata, auxiliary maps, and reference counts.
pub fn verify_integrity(&self) -> Result<(), IntegrityError> {
use IntegrityError as E;
if self.pause_integrity_checks > 0 {
return Ok(());
}
if self.size.rows == 0 {
return Err(E::ZeroRowCount);
}
if self.size.cols == 0 {
return Err(E::ZeroColCount);
}
if self.size.rows > self.capacity.rows || self.size.cols > self.capacity.cols {
return Err(E::InvalidDimensions);
}
let mut styles = HashMap::<u16, usize>::new();
let mut links = HashMap::<u16, usize>::new();
let mut graphemes = 0;
for y in 0..usize::from(self.size.rows) {
let r = self.rows[y];
let base = r.cells();
for x in 0..usize::from(self.size.cols) {
let i = base + x;
let c = self.cells[i];
if c.has_grapheme() {
graphemes += 1;
if !r.grapheme() {
return Err(E::UnmarkedGraphemeRow);
}
let g = self.lookup_grapheme(i).ok_or(E::MissingGraphemeData)?;
if g.is_empty() {
return Err(E::InvalidGraphemeCount);
}
} else if self.grapheme_map.contains_key(&i) {
return Err(E::UnmarkedGraphemeCell);
}
if c.has_styling() {
if !r.styled() {
return Err(E::UnmarkedStyleRow);
}
if self.styles.get(c.style_id()).is_none() {
return Err(E::MissingStyle);
}
*styles.entry(c.style_id()).or_default() += 1;
}
if c.hyperlink() {
if !r.hyperlink() {
return Err(E::UnmarkedHyperlinkRow);
}
let id = self.lookup_hyperlink(i).ok_or(E::MissingHyperlinkData)?;
if self.hyperlink_set.get(id).is_none() {
return Err(E::MissingHyperlinkData);
}
*links.entry(id).or_default() += 1;
} else if self.hyperlink_map.contains_key(&i) {
return Err(E::UnmarkedHyperlinkCell);
}
match c.wide() {
Wide::SpacerTail => {
if x == 0 || self.cells[i - 1].wide() != Wide::Wide {
return Err(E::InvalidSpacerTailLocation);
}
}
Wide::SpacerHead => {
if x + 1 != usize::from(self.size.cols) {
return Err(E::InvalidSpacerHeadLocation);
}
if !r.wrap() {
return Err(E::UnwrappedSpacerHead);
}
}
_ => {}
}
}
}
if graphemes > self.grapheme_count() {
return Err(E::InvalidGraphemeCount);
}
for (id, _, refs) in self.styles.entries() {
if styles.get(&id).copied().unwrap_or(0) > usize::from(refs) {
return Err(E::MismatchedStyleRef);
}
}
for (id, _, refs) in self.hyperlink_set.entries() {
if links.get(&id).copied().unwrap_or(0) > usize::from(refs) {
return Err(E::MismatchedHyperlinkRef);
}
}
Ok(())
}
/// Assert structural integrity in debug/test builds.
pub fn assert_integrity(&self) {
assert_eq!(self.verify_integrity(), Ok(()));
}
}
fn swap_map<T>(map: &mut HashMap<usize, T>, a: usize, b: usize) {
let av = map.remove(&a);
let bv = map.remove(&b);
if let Some(v) = av {
map.insert(b, v);
}
if let Some(v) = bv {
map.insert(a, v);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Color, Flags};
fn page() -> Page {
Page::init(Capacity {
cols: 10,
rows: 10,
styles: 8,
..Capacity::default()
})
.unwrap()
}
fn write(p: &mut Page, x: usize, y: usize, cp: u32) {
let i = p.cell_index(x, y).unwrap();
p.cells[i] = Cell::init(cp);
}
fn cp(p: &Page, x: usize, y: usize) -> u32 {
p.get_row_and_cell(x, y).unwrap().1.codepoint()
}
fn bold() -> Style {
Style {
flags: Flags {
bold: true,
..Flags::default()
},
..Style::default()
}
}
fn link() -> Hyperlink {
Hyperlink {
id: HyperlinkId::Implicit(1),
uri: b"https://example.com".to_vec(),
}
}
fn clone_exact(p: &Page, end: usize) -> Page {
let cap = p.exact_row_capacity(0, end).unwrap();
let mut q = Page::init(cap).unwrap();
q.clone_from(p, 0, end).unwrap();
assert_eq!(q.exact_row_capacity(0, end).unwrap(), cap);
q.assert_integrity();
q
}
#[test]
fn page_layout_can_take_a_maxed_capacity() {
let l = Page::layout(Capacity {
cols: u16::MAX,
rows: u16::MAX,
styles: u16::MAX,
hyperlink_bytes: u16::MAX,
grapheme_bytes: u32::MAX,
string_bytes: u32::MAX,
});
assert!(l.total_size > u32::MAX as usize);
}
#[test]
fn cell_is_zero_by_default() {
let c = Cell::init(0);
assert_eq!(c.raw(), 0);
assert_eq!(c.semantic_content(), SemanticContent::Output);
assert_eq!(std::mem::size_of::<Cell>(), 8);
assert_eq!(std::mem::size_of::<Row>(), 8);
}
#[test]
fn page_capacity_adjust_cols_down() {
let original = Page::layout(STD_CAPACITY).total_size;
let cap = STD_CAPACITY
.adjust(Adjustment {
cols: Some(STD_CAPACITY.cols / 2),
})
.unwrap();
assert_eq!(Page::layout(cap).total_size, original);
assert!(
Page::layout(Capacity {
rows: cap.rows + 1,
..cap
})
.total_size
> original
);
}
#[test]
fn page_capacity_adjust_cols_down_to_1() {
let original = Page::layout(STD_CAPACITY).total_size;
let cap = STD_CAPACITY.adjust(Adjustment { cols: Some(1) }).unwrap();
assert_eq!(Page::layout(cap).total_size, original);
assert!(
Page::layout(Capacity {
rows: cap.rows + 1,
..cap
})
.total_size
> original
);
}
#[test]
fn page_capacity_adjust_cols_up() {
let original = Page::layout(STD_CAPACITY).total_size;
let cap = STD_CAPACITY
.adjust(Adjustment {
cols: Some(STD_CAPACITY.cols * 2),
})
.unwrap();
assert_eq!(Page::layout(cap).total_size, original);
assert!(
Page::layout(Capacity {
rows: cap.rows + 1,
..cap
})
.total_size
> original
);
}
#[test]
fn page_capacity_adjust_cols_sweep() {
let original = Page::layout(STD_CAPACITY).total_size;
let mut cap = STD_CAPACITY;
for cols in 1..STD_CAPACITY.cols * 2 {
cap = cap.adjust(Adjustment { cols: Some(cols) }).unwrap();
assert_eq!(Page::layout(cap).total_size, original);
assert!(
Page::layout(Capacity {
rows: cap.rows + 1,
..cap
})
.total_size
> original
);
}
}
#[test]
fn page_capacity_adjust_cols_too_high() {
assert_eq!(
STD_CAPACITY.adjust(Adjustment {
cols: Some(u16::MAX)
}),
Err(Error::OutOfMemory)
);
}
#[test]
fn capacity_max_cols_basic() {
let max = STD_CAPACITY.max_cols().unwrap();
assert!(max >= STD_CAPACITY.cols);
assert!(
STD_CAPACITY
.adjust(Adjustment { cols: Some(max) })
.unwrap()
.rows
>= 1
);
assert_eq!(
STD_CAPACITY.adjust(Adjustment {
cols: Some(max + 1)
}),
Err(Error::OutOfMemory)
);
}
#[test]
fn capacity_max_cols_preserves_total_size() {
let cap = STD_CAPACITY
.adjust(Adjustment {
cols: STD_CAPACITY.max_cols(),
})
.unwrap();
assert_eq!(
Page::layout(cap).total_size,
Page::layout(STD_CAPACITY).total_size
);
}
#[test]
fn capacity_max_cols_with_1_row_exactly() {
assert_eq!(
STD_CAPACITY
.adjust(Adjustment {
cols: STD_CAPACITY.max_cols()
})
.unwrap()
.rows,
1
);
}
#[test]
fn page_init() {
let p = Page::init(Capacity {
cols: 120,
rows: 80,
styles: 32,
..Capacity::default()
})
.unwrap();
p.assert_integrity();
}
#[test]
fn page_read_and_write_cells() {
let mut p = page();
for y in 0..10 {
write(&mut p, 1, y, y as u32);
}
for y in 0..10 {
assert_eq!(cp(&p, 1, y), y as u32);
}
}
#[test]
fn page_append_grapheme_small() {
let mut p = page();
write(&mut p, 0, 0, 9);
p.append_grapheme(0, 0, 10).unwrap();
assert!(p.rows[0].grapheme());
assert_eq!(p.lookup_grapheme(0), Some([10].as_slice()));
p.append_grapheme(0, 0, 11).unwrap();
assert_eq!(p.lookup_grapheme(0), Some([10, 11].as_slice()));
p.clear_grapheme(0);
p.update_row_grapheme_flag(0);
assert!(!p.rows[0].grapheme());
assert!(!p.cells[0].has_grapheme());
}
#[test]
fn page_append_grapheme_larger_than_chunk() {
let mut p = page();
write(&mut p, 0, 0, 9);
for i in 0..40 {
p.append_grapheme(0, 0, 10 + i).unwrap();
}
assert_eq!(p.lookup_grapheme(0).unwrap(), (10..50).collect::<Vec<_>>());
}
#[test]
fn page_clear_grapheme_not_all_cells() {
let mut p = page();
for x in 0..2 {
write(&mut p, x, 0, 9);
p.append_grapheme(x, 0, 10).unwrap();
}
p.clear_grapheme(0);
p.update_row_grapheme_flag(0);
assert!(p.rows[0].grapheme());
assert!(!p.cells[0].has_grapheme());
assert!(p.cells[1].has_grapheme());
}
#[test]
fn page_clone() {
let mut p = page();
for y in 0..10 {
write(&mut p, 1, y, y as u32);
}
let q = p.clone();
assert_eq!(p.capacity, q.capacity);
for y in 0..10 {
write(&mut p, 1, y, 0);
assert_eq!(cp(&q, 1, y), y as u32);
assert_eq!(cp(&p, 1, y), 0);
}
}
#[test]
fn page_clone_from() {
let mut p = page();
for y in 0..10 {
write(&mut p, 1, y, y as u32);
}
let q = {
let mut q = page();
q.clone_from(&p, 0, 10).unwrap();
q
};
assert_eq!(p.capacity, q.capacity);
for y in 0..10 {
write(&mut p, 1, y, 0);
assert_eq!(cp(&q, 1, y), y as u32);
assert_eq!(cp(&p, 1, y), 0);
}
}
#[test]
fn page_clone_graphemes() {
let mut p = page();
write(&mut p, 0, 0, 9);
p.append_grapheme(0, 0, 10).unwrap();
p.append_grapheme(0, 0, 11).unwrap();
let q = p.clone();
assert!(q.rows[0].grapheme());
assert_eq!(q.lookup_grapheme(0).unwrap(), [10, 11]);
}
#[test]
fn page_clone_styles() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
p.set_style(x, 0, bold()).unwrap();
}
let q = p.clone();
for x in 0..10 {
assert_eq!(p.cells[x].style_id(), q.cells[x].style_id());
assert_eq!(q.style(q.cells[x]), bold());
}
q.assert_integrity();
}
#[test]
fn page_clone_from_shrink_columns() {
let mut p = page();
for y in 0..10 {
write(&mut p, 1, y, y as u32);
}
let mut q = Page::init(Capacity {
cols: 5,
..p.capacity
})
.unwrap();
q.clone_from(&p, 0, 10).unwrap();
assert_eq!(q.size.cols, 5);
for y in 0..10 {
assert_eq!(cp(&q, 1, y), y as u32);
}
}
#[test]
fn page_clone_from_partial() {
let mut p = page();
for y in 0..10 {
write(&mut p, 1, y, y as u32);
}
let mut q = page();
q.clone_from(&p, 0, 5).unwrap();
for y in 0..10 {
assert_eq!(cp(&q, 1, y), if y < 5 { y as u32 } else { 0 });
}
}
#[test]
fn page_clone_from_hyperlinks_exact_capacity() {
let mut p = Page::init(Capacity {
cols: 50,
rows: 50,
..Capacity::default()
})
.unwrap();
let id = p.insert_hyperlink(link()).unwrap();
for n in 0..p.hyperlink_capacity() {
let x = n / 50;
let y = n % 50;
write(&mut p, x, y, 42);
p.hyperlink_set.r#use(id).unwrap();
p.set_hyperlink(x, y, id).unwrap();
}
assert_eq!(p.hyperlink_count(), p.hyperlink_capacity());
let mut q = Page::init(p.capacity).unwrap();
q.clone_from(&p, 0, 50).unwrap();
assert_eq!(q.hyperlink_count(), p.hyperlink_count());
}
#[test]
fn page_clone_from_graphemes() {
let mut p = page();
for y in 0..10 {
write(&mut p, 1, y, y as u32 + 1);
p.append_grapheme(1, y, 10).unwrap();
}
let mut q = page();
q.clone_from(&p, 0, 10).unwrap();
for y in 0..10 {
p.clear_cells(y, 0, 10, false).unwrap();
assert_eq!(cp(&q, 1, y), y as u32 + 1);
assert_eq!(
q.lookup_grapheme(q.cell_index(1, y).unwrap()).unwrap(),
[10]
);
}
q.assert_integrity();
}
#[test]
fn page_clone_from_frees_dst_graphemes() {
let p = page();
let mut q = page();
for y in 0..10 {
write(&mut q, 1, y, 9);
q.append_grapheme(1, y, 10).unwrap();
}
q.clone_from(&p, 0, 10).unwrap();
assert_eq!(q.grapheme_count(), 0);
for y in 0..10 {
assert!(!q.rows[y].grapheme());
}
}
#[test]
fn page_clone_row_from_partial() {
let mut p = page();
let mut q = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
}
q.clone_partial_row_from(&p, 0, 0, 2, 2, 6).unwrap();
for x in 0..10 {
assert_eq!(
cp(&q, x, 0),
if (2..8).contains(&x) { x as u32 + 1 } else { 0 }
);
}
assert_eq!(q.grapheme_count(), 0);
assert!(!q.rows[0].grapheme());
q.assert_integrity();
}
#[test]
fn page_clone_row_from_partial_grapheme_in_non_copied_source_region() {
let mut p = page();
let mut q = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
}
for x in [0, 9] {
p.append_grapheme(x, 0, 10).unwrap();
}
q.clone_partial_row_from(&p, 0, 0, 2, 2, 6).unwrap();
for x in 0..10 {
assert_eq!(
cp(&q, x, 0),
if (2..8).contains(&x) { x as u32 + 1 } else { 0 }
);
}
assert_eq!(q.grapheme_count(), 0);
assert!(!q.rows[0].grapheme());
q.assert_integrity();
}
#[test]
fn page_clone_row_from_partial_grapheme_in_non_copied_dest_region() {
let mut p = page();
let mut q = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
}
for x in 0..10 {
write(&mut q, x, 0, 0xbb);
}
for x in [0, 9] {
q.append_grapheme(x, 0, 10).unwrap();
}
q.clone_partial_row_from(&p, 0, 0, 2, 2, 6).unwrap();
for x in 0..10 {
assert_eq!(
cp(&q, x, 0),
if (2..8).contains(&x) {
x as u32 + 1
} else {
0xbb
}
);
}
assert_eq!(q.grapheme_count(), 2);
assert!(q.rows[0].grapheme());
q.assert_integrity();
}
#[test]
fn page_clone_row_from_partial_hyperlink_in_same_page_copy() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
}
let id = p.insert_hyperlink(link()).unwrap();
p.set_hyperlink(7, 0, id).unwrap();
p.clone_cells_within(0, 1, 2, 2, 8 - 2).unwrap();
for x in 0..10 {
assert_eq!(
cp(&p, x, 1),
if (2..8).contains(&x) { x as u32 + 1 } else { 0 }
);
}
assert_eq!(p.hyperlink_count(), 2);
assert!(p.rows[1].hyperlink());
p.assert_integrity();
}
#[test]
fn page_clone_row_from_partial_hyperlink_in_same_page_omit() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
}
let id = p.insert_hyperlink(link()).unwrap();
p.set_hyperlink(7, 0, id).unwrap();
p.clone_cells_within(0, 1, 2, 2, 6 - 2).unwrap();
for x in 0..10 {
assert_eq!(
cp(&p, x, 1),
if (2..6).contains(&x) { x as u32 + 1 } else { 0 }
);
}
assert_eq!(p.hyperlink_count(), 1);
assert!(!p.rows[1].hyperlink());
p.assert_integrity();
}
#[test]
fn page_move_cells_text_only() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
}
p.move_cells(0, 0, 1, 0, 10).unwrap();
for x in 0..10 {
assert_eq!(cp(&p, x, 0), 0);
assert_eq!(cp(&p, x, 1), x as u32 + 1);
}
p.assert_integrity();
}
#[test]
fn page_move_cells_graphemes() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
p.append_grapheme(x, 0, 10).unwrap();
}
p.move_cells(0, 0, 1, 0, 10).unwrap();
for x in 0..10 {
assert_eq!(cp(&p, x, 0), 0);
assert_eq!(cp(&p, x, 1), x as u32 + 1);
assert_eq!(
p.lookup_grapheme(p.cell_index(x, 1).unwrap()).unwrap(),
[10]
);
}
p.assert_integrity();
}
#[test]
fn page_verify_integrity_graphemes_good() {
let mut p = page();
write(&mut p, 0, 0, 1);
p.append_grapheme(0, 0, 10).unwrap();
p.assert_integrity();
}
#[test]
fn page_verify_integrity_grapheme_row_not_marked() {
let mut p = page();
write(&mut p, 0, 0, 1);
p.append_grapheme(0, 0, 10).unwrap();
p.rows[0].set_grapheme(false);
assert_eq!(
p.verify_integrity(),
Err(IntegrityError::UnmarkedGraphemeRow)
);
}
#[test]
fn page_verify_integrity_styles_good() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
p.set_style(x, 0, bold()).unwrap();
}
p.assert_integrity();
}
#[test]
fn page_verify_integrity_styles_ref_count_mismatch() {
let mut p = page();
for x in 0..10 {
write(&mut p, x, 0, x as u32 + 1);
p.set_style(x, 0, bold()).unwrap();
}
p.styles.release(p.cells[0].style_id()).unwrap();
assert_eq!(
p.verify_integrity(),
Err(IntegrityError::MismatchedStyleRef)
);
}
#[test]
fn page_verify_integrity_zero_rows() {
let mut p = page();
p.size.rows = 0;
assert_eq!(p.verify_integrity(), Err(IntegrityError::ZeroRowCount));
}
#[test]
fn page_verify_integrity_zero_cols() {
let mut p = page();
p.size.cols = 0;
assert_eq!(p.verify_integrity(), Err(IntegrityError::ZeroColCount));
}
#[test]
fn page_exact_row_capacity_empty_rows() {
let p = page();
assert_eq!(
p.exact_row_capacity(0, 5).unwrap(),
Capacity {
cols: 10,
rows: 5,
styles: 0,
hyperlink_bytes: 0,
grapheme_bytes: 0,
string_bytes: 0
}
);
}
#[test]
fn page_exact_row_capacity_styles() {
let mut p = page();
assert_eq!(p.exact_row_capacity(0, 5).unwrap().styles, 0);
p.set_style(0, 0, bold()).unwrap();
assert_eq!(
usize::from(p.exact_row_capacity(0, 5).unwrap().styles),
style::Set::capacity_for_count(1)
);
p.set_style(1, 0, bold()).unwrap();
assert_eq!(
usize::from(p.exact_row_capacity(0, 5).unwrap().styles),
style::Set::capacity_for_count(1)
);
p.set_style(
2,
0,
Style {
flags: Flags {
italic: true,
..Flags::default()
},
..Style::default()
},
)
.unwrap();
p.set_style(
0,
7,
Style {
flags: Flags {
underline: ghostty_vt::sgr::Underline::Single,
..Flags::default()
},
..Style::default()
},
)
.unwrap();
assert_eq!(
usize::from(p.exact_row_capacity(0, 5).unwrap().styles),
style::Set::capacity_for_count(2)
);
assert_eq!(
usize::from(p.exact_row_capacity(0, 10).unwrap().styles),
style::Set::capacity_for_count(3)
);
clone_exact(&p, 5);
}
#[test]
fn page_exact_row_capacity_single_style_clone() {
let mut p = page();
p.set_style(0, 0, bold()).unwrap();
let q = clone_exact(&p, 1);
assert_eq!(q.style(q.cells[0]), bold());
}
#[test]
fn page_exact_row_capacity_styles_max_single_row() {
let mut p = Page::init(Capacity {
cols: u16::MAX,
rows: 1,
styles: u16::MAX,
..Capacity::default()
})
.unwrap();
for x in 0..1000 {
p.set_style(
x,
0,
Style {
fg_color: Color::Rgb(Rgb {
r: x as u8,
g: (x >> 8) as u8,
b: 0,
}),
..Style::default()
},
)
.unwrap();
}
assert_eq!(
usize::from(p.exact_row_capacity(0, 1).unwrap().styles),
style::Set::capacity_for_count(1000)
);
}
#[test]
fn page_exact_row_capacity_grapheme_bytes() {
let mut p = page();
assert_eq!(p.exact_row_capacity(0, 5).unwrap().grapheme_bytes, 0);
for (x, cp0) in [(0, 97), (1, 101), (2, 111)] {
write(&mut p, x, 0, cp0);
p.append_grapheme(x, 0, 0x301).unwrap();
assert_eq!(
p.exact_row_capacity(0, 5).unwrap().grapheme_bytes,
(x as u32 + 1) * 16
);
}
p.append_grapheme(2, 0, 0x302).unwrap();
p.append_grapheme(2, 0, 0x303).unwrap();
write(&mut p, 0, 7, 120);
p.append_grapheme(0, 7, 0x304).unwrap();
assert_eq!(p.exact_row_capacity(0, 5).unwrap().grapheme_bytes, 48);
assert_eq!(p.exact_row_capacity(0, 10).unwrap().grapheme_bytes, 64);
clone_exact(&p, 5);
}
#[test]
fn page_exact_row_capacity_grapheme_bytes_larger_than_chunk() {
let mut p = page();
write(&mut p, 0, 0, 97);
for cp0 in 0x300..0x306 {
p.append_grapheme(0, 0, cp0).unwrap();
}
assert_eq!(p.exact_row_capacity(0, 1).unwrap().grapheme_bytes, 32);
clone_exact(&p, 1);
}
#[test]
fn page_exact_row_capacity_hyperlinks() {
let mut p = Page::init(Capacity {
hyperlink_bytes: 32 * 48,
string_bytes: 512,
..page().capacity
})
.unwrap();
assert_eq!(p.exact_row_capacity(0, 5).unwrap().hyperlink_bytes, 0);
let id = p.insert_hyperlink(link()).unwrap();
p.set_hyperlink(0, 0, id).unwrap();
let one = p.exact_row_capacity(0, 5).unwrap();
assert_eq!(one.string_bytes, 32);
assert_eq!(
usize::from(one.hyperlink_bytes),
hyperlink::Set::capacity_for_count(1) * 48
);
p.hyperlink_set.r#use(id).unwrap();
p.set_hyperlink(1, 0, id).unwrap();
assert_eq!(p.exact_row_capacity(0, 5).unwrap(), one);
let id = p
.insert_hyperlink(Hyperlink {
id: HyperlinkId::Explicit(b"my-link-id".to_vec()),
uri: b"https://other.example.org/path".to_vec(),
})
.unwrap();
p.set_hyperlink(2, 0, id).unwrap();
assert_eq!(p.exact_row_capacity(0, 5).unwrap().string_bytes, 96);
let id = p
.insert_hyperlink(Hyperlink {
id: HyperlinkId::Implicit(99),
uri: b"https://outside.example.com".to_vec(),
})
.unwrap();
p.set_hyperlink(0, 7, id).unwrap();
assert_eq!(p.exact_row_capacity(0, 5).unwrap().string_bytes, 96);
assert_eq!(p.exact_row_capacity(0, 10).unwrap().string_bytes, 128);
clone_exact(&p, 5);
}
#[test]
fn page_exact_row_capacity_single_hyperlink_clone() {
let mut p = page();
let id = p.insert_hyperlink(link()).unwrap();
p.set_hyperlink(0, 0, id).unwrap();
let q = clone_exact(&p, 1);
assert!(q.cells[0].hyperlink());
}
#[test]
fn page_exact_row_capacity_hyperlink_map_capacity_for_many_cells() {
let mut p = Page::init(Capacity {
cols: 50,
rows: 2,
hyperlink_bytes: 32 * 48,
string_bytes: 512,
..Capacity::default()
})
.unwrap();
let id = p.insert_hyperlink(link()).unwrap();
for x in 0..50 {
if x > 0 {
p.hyperlink_set.r#use(id).unwrap();
}
p.set_hyperlink(x, 0, id).unwrap();
}
assert!(p.exact_row_capacity(0, 1).unwrap().hyperlink_bytes >= 4 * 48);
let q = clone_exact(&p, 1);
assert_eq!(q.hyperlink_count(), 50);
}
}
+5119
View File
@@ -0,0 +1,5119 @@
//! Linked terminal pages, byte-bounded history, and stable tracked coordinates.
use crate::{
page::{
self, Adjustment, Capacity, Cell, Error, OwnedCell, Page, Row, SemanticContent,
SemanticPrompt, Wide,
},
point::{Coordinate, Point, Tag},
};
use std::collections::HashMap;
/// Slab index and generation; stale references never alias a reused node.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct NodeId {
index: usize,
generation: u64,
}
impl NodeId {
/// Index in the owning list's slab.
pub fn index(self) -> usize {
self.index
}
/// Generation assigned when the node was allocated.
pub fn generation(self) -> u64 {
self.generation
}
}
/// One node in the page list.
#[derive(Debug)]
pub struct Node {
/// Previous page.
pub prev: Option<NodeId>,
/// Next page.
pub next: Option<NodeId>,
/// Owned terminal page.
pub data: Page,
/// Monotonically increasing allocation serial.
pub serial: u64,
}
/// Untracked cell location, valid until the referenced content is moved.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Pin {
/// Page node.
pub node: NodeId,
/// Row in the page.
pub y: u16,
/// Column in the page.
pub x: u16,
/// History containing this pin was pruned or reset.
pub garbage: bool,
}
/// Handle into the list-owned tracked-pin slab.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct TrackedPin {
index: usize,
generation: u64,
}
#[derive(Debug)]
struct PinSlot {
generation: u64,
pin: Option<Pin>,
}
/// Viewport anchor.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Viewport {
/// Follow the bottom active area.
Active,
/// Follow the oldest history row.
Top,
/// Follow the list's internal tracked viewport pin.
Pin,
}
/// Traversal direction.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Direction {
/// Earlier cells, rows, or pages.
LeftUp,
/// Later cells, rows, or pages.
RightDown,
}
/// Viewport scroll request.
#[derive(Debug, Copy, Clone)]
pub enum Scroll {
/// Show the active area.
Active,
/// Show the oldest history.
Top,
/// Show an absolute screen row.
Row(usize),
/// Move by a signed row count.
DeltaRow(isize),
/// Jump by a signed number of prompts.
DeltaPrompt(isize),
/// Show a specific row, ignoring its column.
Pin(Pin),
}
/// Scrollbar geometry in rows.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct Scrollbar {
/// Total scrollable rows.
pub total: usize,
/// Viewport's offset from the oldest row.
pub offset: usize,
/// Visible row count.
pub len: usize,
}
/// Current cursor position in the active area for resize behavior.
#[derive(Debug, Copy, Clone)]
pub struct Cursor {
/// Cursor column.
pub x: u16,
/// Cursor row.
pub y: u16,
}
/// Resize dimensions and text reflow policy.
#[derive(Debug, Copy, Clone)]
pub struct Resize {
/// New column count, or unchanged.
pub cols: Option<u16>,
/// New row count, or unchanged.
pub rows: Option<u16>,
/// Rewrap logical lines when changing columns.
pub reflow: bool,
/// Cursor position whose surrounding rows should be preserved.
pub cursor: Option<Cursor>,
}
impl Default for Resize {
fn default() -> Self {
Self {
cols: None,
rows: None,
reflow: true,
cursor: None,
}
}
}
/// Auxiliary capacity dimension to double.
#[derive(Debug, Copy, Clone)]
pub enum IncreaseCapacity {
/// Style hash-table slots.
Styles,
/// Grapheme allocation bytes and map entries.
GraphemeBytes,
/// Hyperlink set and map capacity.
HyperlinkBytes,
/// Shared string allocation bytes.
StringBytes,
}
/// Linked pages backed by a reusable node slab and a standard-page pool.
#[derive(Debug)]
pub struct PageList {
nodes: Vec<Option<Node>>,
free_nodes: Vec<usize>,
page_pool: Vec<Page>,
/// Oldest page.
pub first: Option<NodeId>,
/// Last active page.
pub last: Option<NodeId>,
/// Next page serial.
pub page_serial: u64,
/// Lower bound for still-valid serial numbers.
pub page_serial_min: u64,
/// Logical allocated page bytes, including standard pool rounding.
pub page_size: usize,
/// Configured scrollback byte limit.
pub explicit_max_size: usize,
/// Minimum byte limit needed for the active area and one extra page.
pub min_max_size: usize,
/// Total allocated visible rows across all pages.
pub total_rows: usize,
/// Current grid columns.
pub cols: u16,
/// Active grid rows.
pub rows: u16,
/// Current viewport anchor.
pub viewport: Viewport,
pins: Vec<PinSlot>,
free_pins: Vec<usize>,
viewport_pin: Option<TrackedPin>,
pin_serial: u64,
pause_checks: usize,
}
fn std_size() -> usize {
Page::layout(page::STD_CAPACITY).total_size
}
fn allocated_size(cap: Capacity) -> usize {
Page::layout(cap).total_size.max(std_size())
}
fn initial_capacity(cols: u16) -> Capacity {
page::STD_CAPACITY
.adjust(Adjustment { cols: Some(cols) })
.unwrap_or(Capacity {
cols,
..page::STD_CAPACITY
})
}
fn min_max_size(cols: u16, rows: u16) -> usize {
(usize::from(rows).div_ceil(usize::from(initial_capacity(cols).rows)) + 1) * std_size()
}
impl PageList {
/// Create an empty active screen, optionally limiting history in bytes.
pub fn init(cols: u16, rows: u16, max_size: Option<usize>) -> Result<Self, Error> {
if cols == 0 || rows == 0 {
return Err(Error::InvalidDimensions);
}
let mut s = Self {
nodes: Vec::new(),
free_nodes: Vec::new(),
page_pool: Vec::new(),
first: None,
last: None,
page_serial: 0,
page_serial_min: 0,
page_size: 0,
explicit_max_size: max_size.unwrap_or(usize::MAX),
min_max_size: min_max_size(cols, rows),
total_rows: 0,
cols,
rows,
viewport: Viewport::Active,
pins: Vec::new(),
free_pins: Vec::new(),
viewport_pin: None,
pin_serial: 0,
pause_checks: 0,
};
while s.total_rows < usize::from(rows) {
s.grow()?;
}
let pin = s
.get_top_left(Tag::Screen)
.ok_or(Error::InvalidCoordinate)?;
s.viewport_pin = Some(s.track_pin(pin)?);
Ok(s)
}
/// Retrieve a live node, rejecting stale generations.
pub fn node(&self, id: NodeId) -> Option<&Node> {
self.nodes
.get(id.index)?
.as_ref()
.filter(|n| n.serial == id.generation)
}
/// Retrieve a mutable live node, rejecting stale generations.
pub fn node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
self.nodes
.get_mut(id.index)?
.as_mut()
.filter(|n| n.serial == id.generation)
}
fn create_node(&mut self, cap: Capacity) -> Result<NodeId, Error> {
if Page::layout(cap).total_size > u32::MAX as usize {
return Err(Error::OutOfSpace);
}
self.nodes.try_reserve(1).map_err(|_| Error::OutOfMemory)?;
let mut data = if let Some(pos) = self.page_pool.iter().position(|p| p.capacity == cap) {
let mut p = self.page_pool.swap_remove(pos);
p.reinit()?;
p
} else {
Page::init(cap)?
};
data.size.rows = 0;
let index = self.free_nodes.pop().unwrap_or(self.nodes.len());
let serial = self.page_serial;
self.page_serial = self.page_serial.wrapping_add(1);
let id = NodeId {
index,
generation: serial,
};
let node = Node {
prev: None,
next: None,
data,
serial,
};
if index == self.nodes.len() {
self.nodes.push(Some(node));
} else {
self.nodes[index] = Some(node);
}
self.page_size += allocated_size(cap);
Ok(id)
}
fn insert_after(&mut self, prev: Option<NodeId>, id: NodeId) {
let next = prev
.and_then(|p| self.node(p).and_then(|n| n.next))
.or(if prev.is_none() { self.first } else { None });
if let Some(n) = self.node_mut(id) {
n.prev = prev;
n.next = next;
}
if let Some(p) = prev {
if let Some(n) = self.node_mut(p) {
n.next = Some(id);
}
} else {
self.first = Some(id);
}
if let Some(p) = next {
if let Some(n) = self.node_mut(p) {
n.prev = Some(id);
}
} else {
self.last = Some(id);
}
}
fn detach(&mut self, id: NodeId) -> Option<Node> {
let n = self.node(id)?;
let (prev, next) = (n.prev, n.next);
if let Some(p) = prev {
self.node_mut(p)?.next = next;
} else {
self.first = next;
}
if let Some(p) = next {
self.node_mut(p)?.prev = prev;
} else {
self.last = prev;
}
let n = self.nodes[id.index].take()?;
self.page_size -= allocated_size(n.data.capacity);
self.free_nodes.push(id.index);
Some(n)
}
fn recycle(&mut self, node: Node) {
if Page::layout(node.data.capacity).total_size <= std_size() {
self.page_pool.push(node.data);
}
}
fn active_offset(&self) -> usize {
self.total_rows.saturating_sub(usize::from(self.rows))
}
fn screen_row(&self, p: Pin) -> Option<usize> {
if !self.pin_is_valid(p) {
return None;
}
let mut id = self.first;
let mut y = 0;
while let Some(i) = id {
let n = self.node(i)?;
if i == p.node {
return Some(y + usize::from(p.y));
}
y += usize::from(n.data.size.rows);
id = n.next;
}
None
}
fn pin_at(&self, mut y: usize, x: u16) -> Option<Pin> {
if x >= self.cols {
return None;
}
let mut id = self.first;
while let Some(i) = id {
let n = self.node(i)?;
if y < usize::from(n.data.size.rows) {
return Some(Pin {
node: i,
y: y as u16,
x,
garbage: false,
});
}
y -= usize::from(n.data.size.rows);
id = n.next;
}
None
}
/// Current viewport's absolute row offset.
pub fn viewport_row_offset(&self) -> usize {
match self.viewport {
Viewport::Active => self.active_offset(),
Viewport::Top => 0,
Viewport::Pin => self
.viewport_pin
.and_then(|h| self.tracked_pin(h))
.and_then(|p| self.screen_row(p))
.unwrap_or(0),
}
}
fn bounds(&self, tag: Tag) -> (usize, usize) {
match tag {
Tag::Screen => (0, self.total_rows),
Tag::History => (0, self.active_offset()),
Tag::Active => (self.active_offset(), self.total_rows),
Tag::Viewport => {
let start = self.viewport_row_offset();
(start, (start + usize::from(self.rows)).min(self.total_rows))
}
}
}
/// Map a tagged coordinate to an untracked pin.
pub fn pin(&self, pt: Point) -> Option<Pin> {
let (start, end) = self.bounds(pt.tag());
let c = pt.coord();
let y = start.checked_add(c.y as usize)?;
if y >= end {
return None;
}
self.pin_at(y, c.x)
}
/// Convert a pin to a coordinate in a reference frame, if inside it.
pub fn point_from_pin(&self, tag: Tag, p: Pin) -> Option<Point> {
let y = self.screen_row(p)?;
let (start, end) = self.bounds(tag);
if y < start || y >= end {
return None;
}
Some(Point::new(
tag,
Coordinate {
x: p.x,
y: (y - start) as u32,
},
))
}
/// Top-left coordinate in a reference frame, if the region exists.
pub fn get_top_left(&self, tag: Tag) -> Option<Pin> {
self.pin(Point::new(tag, Coordinate::default()))
}
/// Bottom-right coordinate in a reference frame, if the region exists.
pub fn get_bottom_right(&self, tag: Tag) -> Option<Pin> {
let (start, end) = self.bounds(tag);
if start == end {
return None;
}
self.pin_at(end - 1, self.cols - 1)
}
/// Check the generation and logical coordinates of a pin.
pub fn pin_is_valid(&self, p: Pin) -> bool {
self.node(p.node)
.is_some_and(|n| p.x < n.data.size.cols && p.y < n.data.size.rows)
}
/// Register an independently owned location updated by every mutation.
pub fn track_pin(&mut self, p: Pin) -> Result<TrackedPin, Error> {
if !self.pin_is_valid(p) {
return Err(Error::InvalidCoordinate);
}
self.pins.try_reserve(1).map_err(|_| Error::OutOfMemory)?;
let index = self.free_pins.pop().unwrap_or(self.pins.len());
let generation = self.pin_serial;
self.pin_serial = self.pin_serial.wrapping_add(1);
let slot = PinSlot {
generation,
pin: Some(p),
};
if index == self.pins.len() {
self.pins.push(slot);
} else {
self.pins[index] = slot;
}
Ok(TrackedPin { index, generation })
}
/// Read a tracked location. Stale handles return None.
pub fn tracked_pin(&self, h: TrackedPin) -> Option<Pin> {
self.pins
.get(h.index)
.filter(|s| s.generation == h.generation)?
.pin
}
/// Update a tracked location to another valid cell.
pub fn set_tracked_pin(&mut self, h: TrackedPin, p: Pin) -> Result<(), Error> {
if !self.pin_is_valid(p) {
return Err(Error::InvalidCoordinate);
}
let slot = self
.pins
.get_mut(h.index)
.filter(|s| s.generation == h.generation && s.pin.is_some())
.ok_or(Error::InvalidId)?;
slot.pin = Some(p);
Ok(())
}
/// Release a tracked handle. The internal viewport handle remains owned.
pub fn untrack_pin(&mut self, h: TrackedPin) {
if self.viewport_pin == Some(h) {
return;
}
if let Some(s) = self
.pins
.get_mut(h.index)
.filter(|s| s.generation == h.generation && s.pin.is_some())
{
s.pin = None;
self.free_pins.push(h.index);
}
}
/// Count all tracked pins, including the internal viewport pin.
pub fn count_tracked_pins(&self) -> usize {
self.pins.iter().filter(|s| s.pin.is_some()).count()
}
/// Iterate tracked handles and their current locations.
pub fn tracked_pins(&self) -> impl Iterator<Item = (TrackedPin, Pin)> + '_ {
self.pins.iter().enumerate().filter_map(|(index, s)| {
s.pin.map(|p| {
(
TrackedPin {
index,
generation: s.generation,
},
p,
)
})
})
}
/// Actual byte ceiling heuristic, including the minimum active allocation.
pub fn max_size(&self) -> usize {
self.explicit_max_size.max(self.min_max_size)
}
/// Add exactly one row, pruning one whole history page when necessary.
pub fn grow(&mut self) -> Result<Option<NodeId>, Error> {
if let Some(last) = self.last
&& let Some(n) = self.node_mut(last)
&& n.data.size.rows < n.data.capacity.rows
{
n.data.size.rows += 1;
self.total_rows += 1;
return Ok(None);
}
let cap = initial_capacity(self.cols);
if let (Some(first), Some(last)) = (self.first, self.last)
&& first != last
&& self.page_size.saturating_add(std_size()) > self.max_size()
{
let n = self.node(first).ok_or(Error::InvalidId)?;
let removed = usize::from(n.data.size.rows);
let next = n.next;
if self.total_rows - removed + 1 >= usize::from(self.rows) {
let viewport = self.viewport_row_offset();
let n = self.detach(first).ok_or(Error::InvalidId)?;
self.total_rows -= removed;
self.page_serial_min = n.serial.wrapping_add(1);
self.recycle(n);
if let Some(next) = next {
for slot in &mut self.pins {
if let Some(p) = &mut slot.pin
&& p.node == first
{
*p = Pin {
node: next,
x: 0,
y: 0,
garbage: true,
};
}
}
}
if self.viewport == Viewport::Pin && viewport < removed {
self.viewport = Viewport::Top;
}
if let Some(h) = self.viewport_pin
&& let Some(p) = self.pins[h.index].pin.as_mut()
{
p.garbage = false;
}
}
}
let id = self.create_node(cap)?;
self.insert_after(self.last, id);
self.node_mut(id).ok_or(Error::InvalidId)?.data.size.rows = 1;
self.total_rows += 1;
Ok(Some(id))
}
/// Return scrollbar geometry, hiding history when scrollback is disabled.
pub fn scrollbar(&self) -> Scrollbar {
if self.explicit_max_size == 0 {
Scrollbar {
total: usize::from(self.rows),
offset: 0,
len: usize::from(self.rows),
}
} else {
Scrollbar {
total: self.total_rows,
offset: self.viewport_row_offset(),
len: usize::from(self.rows),
}
}
}
/// Move the viewport without allocating pages or creating history.
pub fn scroll(&mut self, behavior: Scroll) {
if self.explicit_max_size == 0 {
self.viewport = Viewport::Active;
return;
}
let row = match behavior {
Scroll::Active => {
self.viewport = Viewport::Active;
return;
}
Scroll::Top => {
self.viewport = Viewport::Top;
return;
}
Scroll::Row(y) => y,
Scroll::DeltaRow(d) => self.viewport_row_offset().saturating_add_signed(d),
Scroll::Pin(p) => {
let Some(y) = self.screen_row(p) else {
return;
};
y
}
Scroll::DeltaPrompt(d) => {
if d == 0 {
return;
}
let current = self.viewport_row_offset();
let mut prompts: Vec<_> = self
.prompt_iterator(
Direction::RightDown,
Point::Screen(Coordinate::default()),
None,
)
.filter_map(|p| self.screen_row(p))
.filter(|y| if d < 0 { *y < current } else { *y > current })
.collect();
if d < 0 {
prompts.reverse();
}
let Some(&y) = prompts.get(
d.unsigned_abs()
.saturating_sub(1)
.min(prompts.len().saturating_sub(1)),
) else {
return;
};
y
}
};
if row >= self.active_offset() {
self.viewport = Viewport::Active;
} else if row == 0 {
self.viewport = Viewport::Top;
} else if let (Some(p), Some(h)) = (self.pin_at(row, 0), self.viewport_pin) {
self.pins[h.index].pin = Some(p);
self.viewport = Viewport::Pin;
}
}
fn fixup_viewport(&mut self) {
if self.viewport == Viewport::Pin && self.viewport_row_offset() >= self.active_offset() {
self.viewport = Viewport::Active;
}
}
/// Read a cell without copying its managed auxiliary values.
pub fn get_cell(&self, pt: Point) -> Option<Cell> {
let p = self.pin(pt)?;
Some(
self.node(p.node)?
.data
.get_row_and_cell(usize::from(p.x), usize::from(p.y))?
.1
.to_owned(),
)
}
/// Access a cell mutably; callers maintain managed metadata and dirty flags.
pub fn get_cell_mut(&mut self, pt: Point) -> Option<&mut Cell> {
let p = self.pin(pt)?;
let page = &mut self.node_mut(p.node)?.data;
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).ok()?;
page.cells.get_mut(i)
}
/// Number of live pages in list order.
pub fn total_pages(&self) -> usize {
self.nodes.iter().filter(|n| n.is_some()).count()
}
/// Clear full-page and individual-row dirty flags.
pub fn clear_dirty(&mut self) {
for n in self.nodes.iter_mut().flatten() {
n.data.dirty = false;
for r in &mut n.data.rows {
r.set_dirty(false);
}
}
}
/// Whether a coordinate's page or row needs redraw.
pub fn is_dirty(&self, pt: Point) -> bool {
self.pin(pt).is_some_and(|p| p.is_dirty(self))
}
/// Split before the pin's row, preserving metadata and remapping tracked pins.
pub fn split(&mut self, p: Pin) -> Result<(), Error> {
let page = &self.node(p.node).ok_or(Error::InvalidId)?.data;
if page.size.rows <= 1 {
return Err(Error::OutOfSpace);
}
if p.y == 0 {
return Ok(());
}
if p.y >= page.size.rows {
return Err(Error::InvalidCoordinate);
}
let mut copy = Page::init(page.capacity)?;
copy.size = page.size;
copy.size.rows -= p.y;
copy.clone_from(page, usize::from(p.y), usize::from(page.size.rows))?;
copy.dirty = page.dirty;
let id = self.create_node(copy.capacity)?;
self.node_mut(id).ok_or(Error::InvalidId)?.data = copy;
self.insert_after(Some(p.node), id);
let src = &mut self.node_mut(p.node).ok_or(Error::InvalidId)?.data;
for y in usize::from(p.y)..usize::from(src.size.rows) {
src.clear_cells(y, 0, usize::from(src.size.cols), false)?;
}
src.size.rows = p.y;
for s in &mut self.pins {
if let Some(pin) = &mut s.pin
&& pin.node == p.node
&& pin.y >= p.y
{
pin.node = id;
pin.y -= p.y;
}
}
Ok(())
}
fn replace_page(&mut self, old: NodeId, page: Page) -> Result<NodeId, Error> {
let prev = self.node(old).ok_or(Error::InvalidId)?.prev;
let id = self.create_node(page.capacity)?;
self.node_mut(id).ok_or(Error::InvalidId)?.data = page;
self.insert_after(prev, id);
let n = self.detach(old).ok_or(Error::InvalidId)?;
self.recycle(n);
for slot in &mut self.pins {
if let Some(p) = &mut slot.pin
&& p.node == old
{
p.node = id;
}
}
Ok(id)
}
/// Double an auxiliary capacity, or rebuild unchanged to reclaim dead IDs.
pub fn increase_capacity(
&mut self,
id: NodeId,
adjustment: Option<IncreaseCapacity>,
) -> Result<NodeId, Error> {
let old = &self.node(id).ok_or(Error::InvalidId)?.data;
let mut cap = old.capacity;
match adjustment {
Some(IncreaseCapacity::Styles) => cap.styles = double16(cap.styles)?,
Some(IncreaseCapacity::GraphemeBytes) => {
cap.grapheme_bytes = double32(cap.grapheme_bytes)?;
}
Some(IncreaseCapacity::HyperlinkBytes) => {
cap.hyperlink_bytes = double16(cap.hyperlink_bytes)?;
}
Some(IncreaseCapacity::StringBytes) => cap.string_bytes = double32(cap.string_bytes)?,
None => {}
}
if Page::layout(cap).total_size > u32::MAX as usize {
return Err(Error::OutOfSpace);
}
let mut page = Page::init(cap)?;
page.size = old.size;
page.clone_from(old, 0, usize::from(old.size.rows))?;
page.dirty = old.dirty;
self.replace_page(id, page)
}
/// Alias for capacity adjustment used by higher-level screen code.
pub fn adjust_capacity(
&mut self,
id: NodeId,
adjustment: Option<IncreaseCapacity>,
) -> Result<NodeId, Error> {
self.increase_capacity(id, adjustment)
}
/// Compact oversized pages when a smaller logical allocation is possible.
pub fn compact(&mut self, id: NodeId) -> Result<Option<NodeId>, Error> {
let old = &self.node(id).ok_or(Error::InvalidId)?.data;
if Page::layout(old.capacity).total_size <= std_size() {
return Ok(None);
}
let cap = old.exact_row_capacity(0, usize::from(old.size.rows))?;
if Page::layout(cap).total_size >= Page::layout(old.capacity).total_size {
return Ok(None);
}
let mut page = Page::init(cap)?;
page.size = old.size;
if page.clone_from(old, 0, usize::from(old.size.rows)).is_err() {
return Ok(None);
}
page.dirty = old.dirty;
self.replace_page(id, page).map(Some)
}
fn erase_range(&mut self, start: usize, end: usize) -> Result<(), Error> {
if start > end || end > self.total_rows {
return Err(Error::InvalidCoordinate);
}
if start == end {
return Ok(());
}
let coords: Vec<_> = self
.tracked_pins()
.filter_map(|(h, p)| self.screen_row(p).map(|y| (h, p, y)))
.collect();
let mut id = self.first;
let mut offset = 0;
while let Some(i) = id {
let n = self.node(i).ok_or(Error::InvalidId)?;
id = n.next;
let count = usize::from(n.data.size.rows);
let a = start.saturating_sub(offset).min(count);
let b = end.saturating_sub(offset).min(count);
offset += count;
if a == b {
continue;
}
if a == 0 && b == count && self.first != self.last {
let n = self.detach(i).ok_or(Error::InvalidId)?;
self.recycle(n);
} else {
let p = &mut self.node_mut(i).ok_or(Error::InvalidId)?.data;
for y in a..b {
p.clear_cells(y, 0, usize::from(p.size.cols), false)?;
}
p.rows[a..count].rotate_left(b - a);
p.size.rows -= (b - a) as u16;
for r in &mut p.rows[a..usize::from(p.size.rows)] {
r.set_dirty(true);
}
}
}
self.total_rows -= end - start;
if self.total_rows == 0 {
self.grow()?;
}
for (h, old, y) in coords {
let erased = y >= start && y < end;
let mapped_y = if y < start {
y
} else if y >= end {
y - (end - start)
} else {
start.min(self.total_rows - 1)
};
let mut p = self
.pin_at(mapped_y, if erased { 0 } else { old.x })
.ok_or(Error::InvalidCoordinate)?;
p.garbage = old.garbage;
self.pins[h.index].pin = Some(p);
}
self.fixup_viewport();
Ok(())
}
/// Remove an inclusive row range, regrowing erased active rows.
pub fn erase_rows(&mut self, start: Point, end: Option<Point>) -> Result<(), Error> {
let a = self
.pin(start)
.and_then(|p| self.screen_row(p))
.ok_or(Error::InvalidCoordinate)?;
let b = if let Some(end) = end {
self.pin(end)
.and_then(|p| self.screen_row(p))
.ok_or(Error::InvalidCoordinate)?
+ 1
} else {
self.bounds(start.tag()).1
};
self.erase_range(a, b)?;
while self.total_rows < usize::from(self.rows) {
self.grow()?;
}
Ok(())
}
/// Remove one row, shifting following rows without changing page dimensions.
pub fn erase_row(&mut self, pt: Point) -> Result<(), Error> {
let p = self.pin(pt).ok_or(Error::InvalidCoordinate)?;
let y = self.screen_row(p).ok_or(Error::InvalidCoordinate)?;
let stationary: Vec<_> = self
.tracked_pins()
.filter(|(_, q)| q.node == p.node && q.y == p.y)
.collect();
self.erase_row_bounded(pt, self.total_rows - y - 1)?;
for (h, p) in stationary {
self.pins[h.index].pin = Some(p);
}
Ok(())
}
/// Remove a row and shift at most `limit` following rows, blanking the last.
pub fn erase_row_bounded(&mut self, pt: Point, limit: usize) -> Result<(), Error> {
let first = self.pin(pt).ok_or(Error::InvalidCoordinate)?;
let start = self.screen_row(first).ok_or(Error::InvalidCoordinate)?;
let end = start.saturating_add(limit).min(self.total_rows - 1);
let old_pins: Vec<_> = self
.tracked_pins()
.filter_map(|(h, p)| self.screen_row(p).map(|y| (h, p, y)))
.collect();
let mut current = first;
loop {
let node = self.node(current.node).ok_or(Error::InvalidId)?;
let offset = self
.screen_row(Pin { y: 0, ..current })
.ok_or(Error::InvalidCoordinate)?;
let stop = (end - offset + 1).min(usize::from(node.data.size.rows));
let next = node.next;
let has_next = offset + stop <= end;
// Rotation moves only compact row metadata and keeps cell indices stable.
let page = &mut self.node_mut(current.node).ok_or(Error::InvalidId)?.data;
page.rows[usize::from(current.y)..stop].rotate_left(1);
page.dirty = true;
if !has_next {
page.clear_cells(stop - 1, 0, usize::from(page.size.cols), false)?;
break;
}
let next = next.ok_or(Error::InvalidId)?;
let source = &self.node(next).ok_or(Error::InvalidId)?.data;
let row = source.rows[0];
let values: Vec<_> = (row.cells()..row.cells() + usize::from(source.size.cols))
.map(|i| source.export_cell(i))
.collect();
let dest = &mut self.node_mut(current.node).ok_or(Error::InvalidId)?.data;
dest.clear_cells(stop - 1, 0, usize::from(dest.size.cols), false)?;
for (x, value) in values.iter().enumerate() {
dest.import_cell(x, stop - 1, value)?;
}
let cells = dest.rows[stop - 1].cells();
dest.rows[stop - 1] = row;
dest.rows[stop - 1].set_cells(cells);
current = Pin {
node: next,
y: 0,
x: 0,
garbage: false,
};
}
for (h, old, y) in old_pins {
if y >= start && y <= end {
let mut p = self
.pin_at(y.saturating_sub(1), if y == 0 { 0 } else { old.x })
.ok_or(Error::InvalidCoordinate)?;
p.garbage = old.garbage;
self.pins[h.index].pin = Some(p);
}
}
self.fixup_viewport();
Ok(())
}
/// Remove cells within a rectangular row region, shifting rows upward.
pub fn erase_cells_bounded(
&mut self,
start: Pin,
end: Pin,
left: u16,
right: u16,
) -> Result<(), Error> {
let a = self.screen_row(start).ok_or(Error::InvalidCoordinate)?;
let b = self.screen_row(end).ok_or(Error::InvalidCoordinate)?;
if a > b || left > right || right >= self.cols {
return Err(Error::InvalidCoordinate);
}
for y in a..b {
let src = self.pin_at(y + 1, left).ok_or(Error::InvalidCoordinate)?;
let dst = self.pin_at(y, left).ok_or(Error::InvalidCoordinate)?;
let data: Vec<_> = {
let page = &self.node(src.node).ok_or(Error::InvalidId)?.data;
(left..=right)
.map(|x| {
page.export_cell(page.rows[usize::from(src.y)].cells() + usize::from(x))
})
.collect()
};
for (x, v) in data.iter().enumerate() {
self.node_mut(dst.node)
.ok_or(Error::InvalidId)?
.data
.import_cell(usize::from(left) + x, usize::from(dst.y), v)?;
}
}
let p = self.pin_at(b, left).ok_or(Error::InvalidCoordinate)?;
self.node_mut(p.node)
.ok_or(Error::InvalidId)?
.data
.clear_cells(
usize::from(p.y),
usize::from(left),
usize::from(right) + 1,
false,
)?;
let changes: Vec<_> = self
.tracked_pins()
.filter_map(|(h, p)| {
self.screen_row(p)
.filter(|y| *y >= a && *y <= b && p.x >= left && p.x <= right)
.map(|y| (h, p, y))
})
.collect();
for (h, p, y) in changes {
if let Some(mut new) = self.pin_at(y.saturating_sub(1).max(a), p.x) {
new.garbage = p.garbage;
self.pins[h.index].pin = Some(new);
}
}
self.fixup_viewport();
Ok(())
}
/// Reset to empty active rows and mark every external tracked pin garbage.
pub fn reset(&mut self) -> Result<(), Error> {
let handles: Vec<_> = self.tracked_pins().map(|(h, _)| h).collect();
let mut id = self.first;
while let Some(i) = id {
let n = self.node(i).ok_or(Error::InvalidId)?;
id = n.next;
let n = self.detach(i).ok_or(Error::InvalidId)?;
self.recycle(n);
}
self.total_rows = 0;
self.viewport = Viewport::Active;
while self.total_rows < usize::from(self.rows) {
self.grow()?;
}
let p = self
.get_top_left(Tag::Screen)
.ok_or(Error::InvalidCoordinate)?;
for h in handles {
self.pins[h.index].pin = Some(Pin {
garbage: self.viewport_pin != Some(h),
..p
});
}
Ok(())
}
/// Scroll the nonempty portion of the active area into history.
pub fn scroll_clear(&mut self) -> Result<(), Error> {
let count = self
.row_iterator(
Direction::LeftUp,
Point::Active(Coordinate::default()),
None,
)
.find(|p| {
self.node(p.node)
.and_then(|n| n.data.get_cells(usize::from(p.y)))
.is_some_and(|cells| cells.iter().any(|c| !c.is_empty()))
})
.and_then(|p| self.point_from_pin(Tag::Active, p))
.map_or(0, |p| p.coord().y as usize + 1);
for _ in 0..count {
self.grow()?;
}
Ok(())
}
/// Erase all scrollback while preserving the active area.
pub fn clear_history(&mut self) -> Result<(), Error> {
let end = self.active_offset();
self.erase_range(0, end)?;
self.viewport = Viewport::Active;
Ok(())
}
/// Nestable suspension of debug integrity checks.
pub fn pause_integrity_checks(&mut self, pause: bool) {
if pause {
self.pause_checks += 1;
} else {
self.pause_checks = self.pause_checks.saturating_sub(1);
}
}
}
/// Result of moving vertically past a list boundary.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Overflow {
/// Movement remained in the screen.
Offset(Pin),
/// Movement reached an edge with rows still remaining.
Overflow {
/// Last valid position, retaining the original column.
end: Pin,
/// Number of rows beyond the boundary.
remaining: usize,
},
}
/// Which part of a pin's row to expose.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CellSubset {
/// Entire visible row.
All,
/// Columns up to and including the pin.
Left,
/// Pin column and all columns to its right.
Right,
}
impl Pin {
/// Read a visible row subset without copying or allocating.
pub fn cells(self, list: &PageList, subset: CellSubset) -> Option<&[Cell]> {
let cells = list.node(self.node)?.data.get_cells(usize::from(self.y))?;
match subset {
CellSubset::All => Some(cells),
CellSubset::Left => cells.get(..=usize::from(self.x)),
CellSubset::Right => cells.get(usize::from(self.x)..),
}
}
/// Move upward, returning the edge and unconsumed distance on overflow.
pub fn up_overflow(self, list: &PageList, n: usize) -> Option<Overflow> {
let y = list.screen_row(self)?;
if n <= y {
Some(Overflow::Offset(self.up(list, n)?))
} else {
Some(Overflow::Overflow {
end: list.pin_at(0, self.x)?,
remaining: n - y,
})
}
}
/// Move downward, returning the edge and unconsumed distance on overflow.
pub fn down_overflow(self, list: &PageList, n: usize) -> Option<Overflow> {
let y = list.screen_row(self)?;
let available = list.total_rows - y - 1;
if n <= available {
Some(Overflow::Offset(self.down(list, n)?))
} else {
Some(Overflow::Overflow {
end: list.pin_at(list.total_rows - 1, self.x)?,
remaining: n - available,
})
}
}
/// Compare page and coordinates, ignoring garbage state.
pub fn eql(self, other: Self) -> bool {
self.node == other.node && self.x == other.x && self.y == other.y
}
/// Read the row and cell from the owning list.
pub fn row_and_cell(self, list: &PageList) -> Option<(&Row, &Cell)> {
list.node(self.node)?
.data
.get_row_and_cell(usize::from(self.x), usize::from(self.y))
}
/// Whether the row or page requires redraw.
pub fn is_dirty(self, list: &PageList) -> bool {
list.node(self.node).is_some_and(|n| {
n.data.dirty
|| n.data
.get_row(usize::from(self.y))
.is_some_and(|r| r.dirty())
})
}
/// Mark the referenced row dirty.
pub fn mark_dirty(self, list: &mut PageList) {
if let Some(r) = list
.node_mut(self.node)
.and_then(|n| n.data.get_row_mut(usize::from(self.y)))
{
r.set_dirty(true);
}
}
/// Resolve the current cell's style.
pub fn style(self, list: &PageList) -> Option<crate::style::Style> {
let page = &list.node(self.node)?.data;
Some(
page.style(
*page
.get_row_and_cell(usize::from(self.x), usize::from(self.y))?
.1,
),
)
}
/// Resolve the current cell's extra codepoints.
pub fn grapheme(self, list: &PageList) -> Option<&[u32]> {
let page = &list.node(self.node)?.data;
page.lookup_grapheme(
page.cell_index(usize::from(self.x), usize::from(self.y))
.ok()?,
)
}
/// Move left within a row, rejecting out-of-range movement.
pub fn left(self, n: usize) -> Option<Self> {
Some(Self {
x: self.x.checked_sub(u16::try_from(n).ok()?)?,
..self
})
}
/// Move right within a row, rejecting out-of-range movement.
pub fn right(self, list: &PageList, n: usize) -> Option<Self> {
let x = usize::from(self.x).checked_add(n)?;
if x >= usize::from(list.cols) {
None
} else {
Some(Self {
x: x as u16,
..self
})
}
}
/// Move left, clamping to column zero.
pub fn left_clamp(self, n: u16) -> Self {
Self {
x: self.x.saturating_sub(n),
..self
}
}
/// Move right, clamping to the last column.
pub fn right_clamp(self, list: &PageList, n: u16) -> Self {
Self {
x: self.x.saturating_add(n).min(list.cols - 1),
..self
}
}
/// Move up across page boundaries while preserving the column.
pub fn up(self, list: &PageList, mut n: usize) -> Option<Self> {
let mut p = self;
loop {
if n <= usize::from(p.y) {
p.y -= n as u16;
return Some(p);
}
n -= usize::from(p.y) + 1;
p.node = list.node(p.node)?.prev?;
p.y = list.node(p.node)?.data.size.rows.checked_sub(1)?;
}
}
/// Move down across page boundaries while preserving the column.
pub fn down(self, list: &PageList, mut n: usize) -> Option<Self> {
let mut p = self;
loop {
let rem = usize::from(list.node(p.node)?.data.size.rows - p.y - 1);
if n <= rem {
p.y += n as u16;
return Some(p);
}
n -= rem + 1;
p.node = list.node(p.node)?.next?;
p.y = 0;
}
}
/// Move left in row-major order, wrapping to previous rows.
pub fn left_wrap(self, list: &PageList, n: usize) -> Option<Self> {
let cols = usize::from(list.cols);
let x = usize::from(self.x);
if n <= x {
return self.left(n);
}
let delta = n - x;
let rows = delta.div_ceil(cols);
let mut p = self.up(list, rows)?;
p.x = ((cols - delta % cols) % cols) as u16;
Some(p)
}
/// Move right in row-major order, wrapping to following rows.
pub fn right_wrap(self, list: &PageList, n: usize) -> Option<Self> {
let x = usize::from(self.x).checked_add(n)?;
let cols = usize::from(list.cols);
let mut p = self.down(list, x / cols)?;
p.x = (x % cols) as u16;
Some(p)
}
/// Whether this pin precedes another pin in row-major screen order.
pub fn before(self, list: &PageList, other: Self) -> bool {
match (list.screen_row(self), list.screen_row(other)) {
(Some(a), Some(b)) => (a, self.x) < (b, other.x),
_ => false,
}
}
/// Whether this pin lies between inclusive endpoints.
pub fn is_between(self, list: &PageList, top: Self, bottom: Self) -> bool {
(self.eql(top) || top.before(list, self)) && (self.eql(bottom) || self.before(list, bottom))
}
/// Move diagonally by signed row and column deltas, clamping columns.
pub fn diagonal(self, list: &PageList, dx: isize, dy: isize) -> Option<Self> {
let mut p = if dy < 0 {
self.up(list, dy.unsigned_abs())?
} else {
self.down(list, dy as usize)?
};
p.x = usize::from(p.x)
.saturating_add_signed(dx)
.min(usize::from(list.cols - 1)) as u16;
Some(p)
}
}
/// A double-ended cell iterator yielding stable untracked locations.
pub struct CellIterator<'a> {
list: &'a PageList,
front: Option<Pin>,
back: Option<Pin>,
direction: Direction,
}
impl CellIterator<'_> {
fn take_front(&mut self) -> Option<Pin> {
let p = self.front?;
if self.back.is_some_and(|b| p.eql(b)) {
self.front = None;
self.back = None;
} else {
self.front = p.right_wrap(self.list, 1);
}
Some(p)
}
fn take_back(&mut self) -> Option<Pin> {
let p = self.back?;
if self.front.is_some_and(|a| p.eql(a)) {
self.front = None;
self.back = None;
} else {
self.back = p.left_wrap(self.list, 1);
}
Some(p)
}
}
impl Iterator for CellIterator<'_> {
type Item = Pin;
fn next(&mut self) -> Option<Pin> {
if self.direction == Direction::RightDown {
self.take_front()
} else {
self.take_back()
}
}
}
impl DoubleEndedIterator for CellIterator<'_> {
fn next_back(&mut self) -> Option<Pin> {
if self.direction == Direction::RightDown {
self.take_back()
} else {
self.take_front()
}
}
}
/// A double-ended row iterator yielding column-zero pins.
pub struct RowIterator<'a> {
list: &'a PageList,
front: Option<Pin>,
back: Option<Pin>,
direction: Direction,
}
impl RowIterator<'_> {
fn take_front(&mut self) -> Option<Pin> {
let p = self.front?;
if self.back.is_some_and(|b| p.eql(b)) {
self.front = None;
self.back = None;
} else {
self.front = p.down(self.list, 1);
}
Some(p)
}
fn take_back(&mut self) -> Option<Pin> {
let p = self.back?;
if self.front.is_some_and(|a| p.eql(a)) {
self.front = None;
self.back = None;
} else {
self.back = p.up(self.list, 1);
}
Some(p)
}
}
impl Iterator for RowIterator<'_> {
type Item = Pin;
fn next(&mut self) -> Option<Pin> {
if self.direction == Direction::RightDown {
self.take_front()
} else {
self.take_back()
}
}
}
impl DoubleEndedIterator for RowIterator<'_> {
fn next_back(&mut self) -> Option<Pin> {
if self.direction == Direction::RightDown {
self.take_back()
} else {
self.take_front()
}
}
}
/// A contiguous half-open row range within one page.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Chunk {
/// Page node.
pub node: NodeId,
/// First row included.
pub start: u16,
/// First row excluded.
pub end: u16,
}
impl Chunk {
/// Row metadata covered by this chunk.
pub fn rows(self, list: &PageList) -> Option<&[Row]> {
list.node(self.node)?
.data
.rows
.get(usize::from(self.start)..usize::from(self.end))
}
/// True if all logical rows in the page are covered.
pub fn full_page(self, list: &PageList) -> bool {
self.start == 0
&& list
.node(self.node)
.is_some_and(|n| self.end == n.data.size.rows)
}
/// True when two chunks overlap in the same page.
pub fn overlaps(self, other: Self) -> bool {
self.node == other.node && self.start < other.end && other.start < self.end
}
}
/// A double-ended page iterator yielding maximal contiguous row chunks.
pub struct PageIterator<'a> {
rows: RowIterator<'a>,
}
impl PageIterator<'_> {
fn front(&mut self) -> Option<Chunk> {
let a = self.rows.front?;
let b = self.rows.back?;
let end = if a.node == b.node {
b.y + 1
} else {
self.rows.list.node(a.node)?.data.size.rows
};
let c = Chunk {
node: a.node,
start: a.y,
end,
};
if a.node == b.node {
self.rows.front = None;
self.rows.back = None;
} else {
self.rows.front = Some(Pin {
node: self.rows.list.node(a.node)?.next?,
y: 0,
x: 0,
garbage: false,
});
}
Some(c)
}
fn back(&mut self) -> Option<Chunk> {
let a = self.rows.front?;
let b = self.rows.back?;
let start = if a.node == b.node { a.y } else { 0 };
let c = Chunk {
node: b.node,
start,
end: b.y + 1,
};
if a.node == b.node {
self.rows.front = None;
self.rows.back = None;
} else {
let node = self.rows.list.node(b.node)?.prev?;
self.rows.back = Some(Pin {
node,
y: self.rows.list.node(node)?.data.size.rows - 1,
x: 0,
garbage: false,
});
}
Some(c)
}
}
impl Iterator for PageIterator<'_> {
type Item = Chunk;
fn next(&mut self) -> Option<Chunk> {
if self.rows.direction == Direction::RightDown {
self.front()
} else {
self.back()
}
}
}
impl DoubleEndedIterator for PageIterator<'_> {
fn next_back(&mut self) -> Option<Chunk> {
if self.rows.direction == Direction::RightDown {
self.back()
} else {
self.front()
}
}
}
impl PageList {
fn iter_bounds(&self, start: Point, end: Option<Point>) -> (Option<Pin>, Option<Pin>) {
let a = self.pin(start);
let b = end.map_or_else(
|| self.get_bottom_right(start.tag()),
|p| {
if p.tag() == start.tag() {
self.pin(p)
} else {
None
}
},
);
if let (Some(a), Some(b)) = (a, b)
&& (a.eql(b) || a.before(self, b))
{
return (Some(a), Some(b));
}
(None, None)
}
/// Iterate cells in an inclusive tagged range; None means the region's end.
pub fn cell_iterator(
&self,
direction: Direction,
start: Point,
end: Option<Point>,
) -> CellIterator<'_> {
let (front, back) = self.iter_bounds(start, end);
CellIterator {
list: self,
front,
back,
direction,
}
}
/// Iterate rows in an inclusive tagged range, ignoring endpoint columns.
pub fn row_iterator(
&self,
direction: Direction,
start: Point,
end: Option<Point>,
) -> RowIterator<'_> {
let (front, back) = self.iter_bounds(start, end);
RowIterator {
list: self,
front: front.map(|p| Pin { x: 0, ..p }),
back: back.map(|p| Pin { x: 0, ..p }),
direction,
}
}
/// Iterate maximal page chunks in an inclusive tagged range.
pub fn page_iterator(
&self,
direction: Direction,
start: Point,
end: Option<Point>,
) -> PageIterator<'_> {
PageIterator {
rows: self.row_iterator(direction, start, end),
}
}
/// Iterate prompt starts, coalescing continuation rows in either direction.
pub fn prompt_iterator(
&self,
direction: Direction,
start: Point,
end: Option<Point>,
) -> PromptIterator<'_> {
let (front, back) = self.iter_bounds(start, end);
PromptIterator {
list: self,
current: if direction == Direction::RightDown {
front
} else {
back
},
limit: if direction == Direction::RightDown {
back
} else {
front
},
direction,
}
}
/// Locate prompt, input, or output bounds within the prompt starting at `at`.
pub fn highlight_semantic_content(
&self,
at: Pin,
content: SemanticContent,
) -> Option<(Pin, Pin)> {
let rowstart = Pin { x: 0, ..at };
let start_pt = self.point_from_pin(Tag::Screen, rowstart)?;
let mut prompts = self.prompt_iterator(Direction::RightDown, start_pt, None);
prompts.next()?;
let end = prompts
.next()
.and_then(|p| p.up(self, 1))
.map(|p| Pin {
x: self.cols - 1,
..p
})
.or_else(|| self.get_bottom_right(Tag::Screen))?;
let mut it = CellIterator {
list: self,
front: Some(at),
back: Some(end),
direction: Direction::RightDown,
};
match content {
SemanticContent::Prompt => {
let mut last = at;
for p in it {
if p.row_and_cell(self)?.1.semantic_content() == SemanticContent::Output {
break;
}
last = p;
}
Some((rowstart, last))
}
SemanticContent::Input => {
let first = loop {
let p = it.next()?;
match p.row_and_cell(self)?.1.semantic_content() {
SemanticContent::Prompt => {}
SemanticContent::Output => return None,
SemanticContent::Input => break p,
}
};
let mut last = first;
for p in it {
match p.row_and_cell(self)?.1.semantic_content() {
SemanticContent::Prompt => {}
SemanticContent::Output => break,
SemanticContent::Input => last = p,
}
}
Some((first, last))
}
SemanticContent::Output => {
let first = loop {
let p = it.next()?;
let c = p.row_and_cell(self)?.1;
if c.semantic_content() == SemanticContent::Output && c.has_text() {
break p;
}
};
let mut last = first;
for p in it {
let c = p.row_and_cell(self)?.1;
if c.semantic_content() != SemanticContent::Output {
break;
}
if c.has_text() {
last = p;
}
}
Some((first, last))
}
}
}
}
/// Iterator over primary prompts and orphaned prompt continuations.
pub struct PromptIterator<'a> {
list: &'a PageList,
current: Option<Pin>,
limit: Option<Pin>,
direction: Direction,
}
impl Iterator for PromptIterator<'_> {
type Item = Pin;
fn next(&mut self) -> Option<Pin> {
let mut current = self.current?;
loop {
let at_limit = self
.limit
.is_some_and(|p| p.node == current.node && p.y == current.y);
let kind = current.row_and_cell(self.list)?.0.semantic_prompt();
if kind != SemanticPrompt::None {
let result = Pin { x: 0, ..current };
if at_limit {
self.current = None;
return Some(result);
}
if self.direction == Direction::RightDown {
let mut next = current.down(self.list, 1);
while let Some(p) = next {
if p.row_and_cell(self.list)?.0.semantic_prompt()
!= SemanticPrompt::PromptContinuation
{
self.current = Some(p);
return Some(result);
}
if self.limit.is_some_and(|l| l.node == p.node && l.y == p.y) {
break;
}
next = p.down(self.list, 1);
}
self.current = None;
return Some(result);
}
if kind == SemanticPrompt::Prompt {
self.current = current.up(self.list, 1);
return Some(result);
}
let mut end = current;
while let Some(prior) = end.up(self.list, 1) {
if self
.limit
.is_some_and(|p| p.node == prior.node && p.y == prior.y)
{
break;
}
match prior.row_and_cell(self.list)?.0.semantic_prompt() {
SemanticPrompt::None => {
self.current = Some(prior);
return Some(Pin { x: 0, ..end });
}
SemanticPrompt::Prompt => {
self.current = prior.up(self.list, 1);
return Some(Pin { x: 0, ..prior });
}
SemanticPrompt::PromptContinuation => end = prior,
}
}
self.current = None;
return Some(result);
}
if at_limit {
self.current = None;
return None;
}
let next = if self.direction == Direction::RightDown {
current.down(self.list, 1)
} else {
current.up(self.list, 1)
};
let Some(next) = next else {
self.current = None;
return None;
};
current = next;
}
}
}
#[derive(Clone)]
struct SavedRow {
row: Row,
cells: Vec<OwnedCell>,
}
impl PageList {
fn snapshot(&self) -> Vec<SavedRow> {
self.row_iterator(
Direction::RightDown,
Point::Screen(Coordinate::default()),
None,
)
.filter_map(|p| {
let page = &self.node(p.node)?.data;
let mut row = *page.get_row(usize::from(p.y))?;
row.set_dirty(row.dirty() || page.dirty);
let cells = (row.cells()..row.cells() + usize::from(page.size.cols))
.map(|i| page.export_cell(i))
.collect();
Some(SavedRow { row, cells })
})
.collect()
}
fn build_pages(cols: u16, rows: &[SavedRow], mark_dirty: bool) -> Result<Vec<Page>, Error> {
let mut pages = Vec::new();
let mut offset = 0;
while offset < rows.len() {
let mut cap = initial_capacity(cols);
let count = usize::from(cap.rows).min(rows.len() - offset);
// Count required values before allocating, so cloning is monotonic.
let mut styles = std::collections::HashSet::new();
let mut links = std::collections::HashSet::new();
let mut gb = 0usize;
let mut sb = 0usize;
let mut hc = 0usize;
for r in &rows[offset..offset + count] {
for c in &r.cells {
if let Some(s) = c.style {
styles.insert(s);
}
if let Some(g) = &c.grapheme {
gb += (g.len() * 4).next_multiple_of(16);
}
if let Some(h) = &c.hyperlink {
hc += 1;
if links.insert(h.clone()) {
sb += h.string_bytes();
}
}
}
}
let sc = crate::style::Set::capacity_for_count(styles.len());
let lc = crate::hyperlink::Set::capacity_for_count(links.len()).max(hc.div_ceil(16));
cap.styles = cap
.styles
.max(u16::try_from(sc).map_err(|_| Error::OutOfSpace)?);
cap.hyperlink_bytes = cap
.hyperlink_bytes
.max(u16::try_from(lc * 48).map_err(|_| Error::OutOfSpace)?);
cap.grapheme_bytes = cap
.grapheme_bytes
.max(u32::try_from(gb).map_err(|_| Error::OutOfSpace)?);
cap.string_bytes = cap
.string_bytes
.max(u32::try_from(sb).map_err(|_| Error::OutOfSpace)?);
let mut page = Page::init(cap)?;
page.size.rows = count as u16;
for (y, row) in rows[offset..offset + count].iter().enumerate() {
for (x, c) in row.cells.iter().take(usize::from(cols)).enumerate() {
page.import_cell(x, y, c)?;
}
let start = page.rows[y].cells();
page.rows[y] = row.row;
page.rows[y].set_cells(start);
page.rows[y].set_dirty(mark_dirty || row.row.dirty());
page.update_row_grapheme_flag(y);
page.update_row_styled_flag(y);
page.update_row_hyperlink_flag(y);
page.rows[y].set_kitty_virtual_placeholder(
row.cells.iter().any(|c| c.cell.codepoint() == 0x10eeee),
);
}
pages.push(page);
offset += count;
}
Ok(pages)
}
fn install_pages(
&mut self,
pages: Vec<Page>,
cols: u16,
coords: &[(TrackedPin, usize, u16, bool)],
) -> Result<(), Error> {
let old: Vec<_> = self
.page_iterator(
Direction::RightDown,
Point::Screen(Coordinate::default()),
None,
)
.map(|c| c.node)
.collect();
let mut added = Vec::new();
for page in pages {
let id = match self.create_node(page.capacity) {
Ok(id) => id,
Err(e) => {
for id in added {
if let Some(n) = self.detach(id) {
self.recycle(n);
}
}
return Err(e);
}
};
self.node_mut(id).ok_or(Error::InvalidId)?.data = page;
self.insert_after(self.last, id);
added.push(id);
}
for id in old {
if let Some(n) = self.detach(id) {
self.recycle(n);
}
}
self.cols = cols;
self.total_rows = self
.nodes
.iter()
.flatten()
.map(|n| usize::from(n.data.size.rows))
.sum();
for &(h, y, x, garbage) in coords {
if let Some(mut p) =
self.pin_at(y.min(self.total_rows.saturating_sub(1)), x.min(cols - 1))
{
p.garbage = garbage;
self.pins[h.index].pin = Some(p);
}
}
self.fixup_viewport();
Ok(())
}
fn trim_blank_rows(&mut self, limit: usize) -> Result<(), Error> {
for _ in 0..limit {
if self.total_rows <= 1 {
break;
}
let last = self
.pin_at(self.total_rows - 1, 0)
.ok_or(Error::InvalidCoordinate)?;
let page = &self.node(last.node).ok_or(Error::InvalidId)?.data;
if page
.get_cells(usize::from(last.y))
.is_some_and(|c| c.iter().any(|c| !c.is_empty()))
|| self
.tracked_pins()
.any(|(_, p)| p.node == last.node && p.y == last.y)
{
break;
}
self.erase_range(self.total_rows - 1, self.total_rows)?;
}
Ok(())
}
/// Resize the active area and optionally reflow wrapped logical lines.
pub fn resize(&mut self, opts: Resize) -> Result<(), Error> {
let cols = opts.cols.unwrap_or(self.cols);
let rows = opts.rows.unwrap_or(self.rows);
if cols == 0 || rows == 0 {
return Err(Error::InvalidDimensions);
}
let cursor = opts.cursor.and_then(|c| {
self.pin(Point::Active(Coordinate {
x: c.x,
y: u32::from(c.y),
}))
});
let cursor_handle = cursor.map(|p| self.track_pin(p)).transpose()?;
let result = self.resize_inner(opts, cols, rows, cursor_handle);
if let Some(h) = cursor_handle {
self.untrack_pin(h);
}
result
}
fn resize_inner(
&mut self,
opts: Resize,
cols: u16,
rows: u16,
cursor_handle: Option<TrackedPin>,
) -> Result<(), Error> {
let old_rows = self.rows;
self.min_max_size = min_max_size(cols, rows);
if cols != self.cols {
if !opts.reflow && cols < self.cols {
for n in self.nodes.iter_mut().flatten() {
let p = &mut n.data;
for y in 0..usize::from(p.size.rows) {
p.clear_cells(y, usize::from(cols), usize::from(p.size.cols), false)?;
}
p.size.cols = cols;
}
self.cols = cols;
for slot in &mut self.pins {
if let Some(p) = &mut slot.pin {
p.x = p.x.min(cols - 1);
}
}
} else {
let old = self.snapshot();
let tracked: Vec<_> = self
.tracked_pins()
.filter_map(|(h, p)| self.screen_row(p).map(|y| (h, y, p.x, p.garbage)))
.collect();
let mut remapped = tracked.clone();
let mut output = Vec::<SavedRow>::new();
if opts.reflow {
let mut current = SavedRow {
row: Row::default(),
cells: Vec::new(),
};
for (y, row) in old.iter().enumerate() {
if current.cells.is_empty() {
current.row = row.row;
current.row.set_wrap(false);
current
.row
.set_wrap_continuation(y > 0 && old[y - 1].row.wrap());
}
let mut len = if row.row.wrap() {
row.cells.len()
} else {
row.cells
.iter()
.rposition(|c| !c.cell.is_empty())
.map_or(0, |x| x + 1)
};
if let Some(h) = cursor_handle
&& let Some((_, _, x, _)) =
tracked.iter().find(|(th, ty, _, _)| *th == h && *ty == y)
{
len = len.max(usize::from(*x) + 1);
}
let mut x = 0;
while x < len {
let c = &row.cells[x];
if c.cell.wide() == Wide::SpacerHead {
x += 1;
continue;
}
let wide = c.cell.wide() == Wide::Wide;
let width = if wide && cols > 1 { 2 } else { 1 };
if current.cells.len() + width > usize::from(cols) {
if wide && current.cells.len() < usize::from(cols) {
let mut spacer = Cell::default();
spacer.set_wide(Wide::SpacerHead);
current.cells.push(OwnedCell {
cell: spacer,
style: None,
grapheme: None,
hyperlink: None,
});
}
current.row.set_wrap(true);
output.push(current);
let mut meta = row.row;
meta.set_wrap(false);
meta.set_wrap_continuation(true);
current = SavedRow {
row: meta,
cells: Vec::new(),
};
}
let newx = current.cells.len() as u16;
let newy = output.len();
for (index, (_, ty, tx, _)) in tracked.iter().enumerate() {
if *ty == y && usize::from(*tx) >= x && usize::from(*tx) < x + width
{
remapped[index].1 = newy;
remapped[index].2 = newx + (*tx - x as u16);
}
}
let mut value = c.clone();
if wide && cols == 1 {
value.cell = Cell::default();
value.style = None;
value.grapheme = None;
value.hyperlink = None;
}
current.cells.push(value);
if wide && cols > 1 {
let tail = row
.cells
.get(x + 1)
.filter(|v| v.cell.wide() == Wide::SpacerTail)
.cloned()
.unwrap_or_else(|| {
let mut cell = Cell::default();
cell.set_wide(Wide::SpacerTail);
OwnedCell {
cell,
style: None,
grapheme: None,
hyperlink: None,
}
});
current.cells.push(tail);
}
x += if wide { 2 } else { 1 };
}
for (index, (_, ty, tx, _)) in tracked.iter().enumerate() {
if *ty == y && usize::from(*tx) >= len {
remapped[index].1 = output.len();
remapped[index].2 = (current.cells.len() + usize::from(*tx) - len)
.min(usize::from(cols - 1))
as u16;
}
}
if !row.row.wrap() {
current.row.set_wrap(false);
output.push(current);
current = SavedRow {
row: Row::default(),
cells: Vec::new(),
};
}
}
if !current.cells.is_empty() {
output.push(current);
}
} else {
for row in &old {
let mut r = row.clone();
r.cells.truncate(usize::from(cols));
for c in &mut r.cells {
if c.cell.wide() == Wide::SpacerHead {
c.cell.set_wide(Wide::Narrow);
c.cell.set_codepoint(0);
c.grapheme = None;
}
}
output.push(r);
}
}
if opts.reflow {
let mut minimum = usize::from(rows);
for (h, y, _, _) in &remapped {
minimum = minimum.max(y + 1);
if Some(*h) == cursor_handle {
minimum = minimum.max(
y + 1
+ opts.cursor.map_or(0, |c| {
usize::from(old_rows.saturating_sub(c.y + 1))
}),
);
}
}
while output.len() > minimum
&& output.last().is_some_and(|r| {
r.row.semantic_prompt() == SemanticPrompt::None
&& r.cells.iter().all(|c| c.cell.is_empty())
})
{
output.pop();
}
}
while output.len() < usize::from(rows) {
output.push(SavedRow {
row: Row::default(),
cells: Vec::new(),
});
}
let pages = Self::build_pages(cols, &output, true)?;
self.install_pages(pages, cols, &remapped)?;
}
}
if rows < old_rows {
self.trim_blank_rows(usize::from(old_rows - rows))?;
}
self.rows = rows;
if rows > old_rows && opts.cursor.is_some_and(|c| c.y < old_rows - 1) {
for _ in 0..rows - old_rows {
self.grow()?;
}
}
while self.total_rows < usize::from(rows) {
self.grow()?;
}
self.fixup_viewport();
Ok(())
}
/// Clone an inclusive cell range into an independent list and remap tracked pins.
pub fn clone_range(
&self,
start: Point,
end: Option<Point>,
) -> Result<(Self, HashMap<TrackedPin, TrackedPin>), Error> {
let (a, b) = self.iter_bounds(start, end);
let a = Pin {
x: 0,
..a.ok_or(Error::InvalidCoordinate)?
};
let b = Pin {
x: self.cols - 1,
..b.ok_or(Error::InvalidCoordinate)?
};
let ay = self.screen_row(a).ok_or(Error::InvalidCoordinate)?;
let by = self.screen_row(b).ok_or(Error::InvalidCoordinate)?;
let mut saved = self.snapshot()[ay..=by].to_vec();
let blank = || OwnedCell {
cell: Cell::default(),
style: None,
grapheme: None,
hyperlink: None,
};
for c in &mut saved[0].cells[..usize::from(a.x)] {
*c = blank();
}
if let Some(last) = saved.last_mut() {
for c in &mut last.cells[usize::from(b.x) + 1..] {
*c = blank();
}
}
while saved.len() < usize::from(self.rows) {
saved.push(SavedRow {
row: Row::default(),
cells: Vec::new(),
});
}
let mut result = Self::init(self.cols, self.rows, Some(self.explicit_max_size))?;
let pages = Self::build_pages(self.cols, &saved, false)?;
let vh = result.viewport_pin.ok_or(Error::InvalidId)?;
result.install_pages(pages, self.cols, &[(vh, 0, 0, false)])?;
let mut remap = HashMap::new();
for (h, p) in self.tracked_pins() {
if Some(h) == self.viewport_pin {
continue;
}
if p.is_between(self, a, b) {
let y = self.screen_row(p).ok_or(Error::InvalidCoordinate)?;
let mut q = result.pin_at(y - ay, p.x).ok_or(Error::InvalidCoordinate)?;
q.garbage = p.garbage;
remap.insert(h, result.track_pin(q)?);
}
}
Ok((result, remap))
}
/// Fallibly clone the whole screen with independently owned tracked handles.
pub fn try_clone(&self) -> Result<Self, Error> {
let (mut result, remap) = self.clone_range(Point::Screen(Coordinate::default()), None)?;
for handle in remap.into_values() {
result.untrack_pin(handle);
}
Ok(result)
}
/// Debug-friendly UTF-8 rows, omitting wide spacers and rendering blanks as spaces.
pub fn diagram(&self) -> String {
let mut s = String::new();
for p in self.row_iterator(
Direction::RightDown,
Point::Screen(Coordinate::default()),
None,
) {
if let Some(n) = self.node(p.node)
&& let Some(cells) = n.data.get_cells(usize::from(p.y))
{
for c in cells {
if matches!(c.wide(), Wide::SpacerHead | Wide::SpacerTail) {
continue;
}
s.push(
char::from_u32(c.codepoint())
.filter(|c| *c != '\0')
.unwrap_or(' '),
);
}
s.push('\n');
}
}
s
}
}
#[cfg(any(debug_assertions, test))]
impl PageList {
/// Verify list links, byte/row accounting, every page, and all tracked pins.
pub fn verify_integrity(&self) -> Result<(), String> {
if self.pause_checks > 0 {
return Ok(());
}
let mut prev = None;
let mut id = self.first;
let mut rows = 0;
let mut bytes = 0;
let mut count = 0;
while let Some(i) = id {
let n = self.node(i).ok_or("stale node")?;
if n.prev != prev {
return Err("broken previous link".into());
}
n.data
.verify_integrity()
.map_err(|e| format!("page: {e:?}"))?;
rows += usize::from(n.data.size.rows);
bytes += allocated_size(n.data.capacity);
count += 1;
if count > self.nodes.len() {
return Err("cycle".into());
}
prev = id;
id = n.next;
}
if prev != self.last
|| rows != self.total_rows
|| bytes != self.page_size
|| count != self.total_pages()
{
return Err("list accounting mismatch".into());
}
if rows < usize::from(self.rows) {
return Err("active rows missing".into());
}
for (_, p) in self.tracked_pins() {
if !self.pin_is_valid(p) {
return Err("invalid tracked pin".into());
}
}
Ok(())
}
/// Assert all list invariants in debug/test builds.
pub fn assert_integrity(&self) {
assert_eq!(self.verify_integrity(), Ok(()));
}
}
fn double16(v: u16) -> Result<u16, Error> {
if v == u16::MAX {
return Err(Error::OutOfSpace);
}
Ok(v.saturating_mul(2).max(1))
}
fn double32(v: u32) -> Result<u32, Error> {
if v == u32::MAX {
return Err(Error::OutOfSpace);
}
Ok(v.saturating_mul(2).max(1))
}
#[cfg(test)]
mod tests {
use super::*;
fn screen(x: u16, y: u32) -> Point {
Point::Screen(Coordinate { x, y })
}
fn active(x: u16, y: u32) -> Point {
Point::Active(Coordinate { x, y })
}
fn grow(s: &mut PageList, n: usize) {
for _ in 0..n {
s.grow().unwrap();
}
}
fn fill(s: &mut PageList) {
for y in 0..s.total_rows {
for x in 0..s.cols {
*s.get_cell_mut(screen(x, y as u32)).unwrap() = Cell::init(u32::from(b'A'));
}
}
}
fn write_rows(s: &mut PageList) {
for y in 0..s.total_rows {
*s.get_cell_mut(screen(0, y as u32)).unwrap() = Cell::init(y as u32);
}
}
fn wraps(s: &mut PageList) {
for y in 0..s.total_rows {
let p = s.pin(screen(0, y as u32)).unwrap();
let r = s
.node_mut(p.node)
.unwrap()
.data
.get_row_mut(usize::from(p.y))
.unwrap();
r.set_wrap(y % 2 == 0);
r.set_wrap_continuation(y % 2 != 0);
}
}
fn finish(s: &PageList) {
s.assert_integrity();
}
fn first_cap(s: &PageList) -> usize {
usize::from(s.node(s.first.unwrap()).unwrap().data.capacity.rows)
}
fn fill_pages(s: &mut PageList, n: usize) {
let target = first_cap(s) * n;
grow(s, target - s.total_rows);
}
#[test]
fn page_list() {
let s = PageList::init(80, 24, None).unwrap();
assert_eq!(s.viewport, Viewport::Active);
assert_eq!(s.total_rows, 24);
assert_eq!(s.get_top_left(Tag::Active).unwrap().node, s.first.unwrap());
assert_eq!(
s.scrollbar(),
Scrollbar {
total: 24,
offset: 0,
len: 24
}
);
assert_eq!(
s.tracked_pin(s.viewport_pin.unwrap()).unwrap().node,
s.first.unwrap()
);
finish(&s);
}
#[test]
fn page_list_init_rows_across_two_pages() {
let mut cols = 50;
while initial_capacity(cols).rows >= 100 {
cols += 50;
}
let s = PageList::init(cols, 100, None).unwrap();
assert_eq!(s.total_pages(), 2);
assert_eq!(s.total_rows, 100);
assert_eq!(
s.scrollbar(),
Scrollbar {
total: 100,
offset: 0,
len: 100
}
);
finish(&s);
}
#[test]
fn page_list_init_more_than_max_cols() {
let cols = page::STD_CAPACITY.max_cols().unwrap() + 1;
let s = PageList::init(cols, 24, None).unwrap();
assert_eq!(s.cols, cols);
assert_eq!(s.total_rows, 24);
assert!(s.page_size > std_size());
finish(&s);
}
#[test]
fn page_list_point_from_pin_active_no_history() {
let s = PageList::init(80, 24, None).unwrap();
for (x, y) in [(0, 0), (4, 2)] {
let p = s.pin(screen(x, y)).unwrap();
assert_eq!(s.point_from_pin(Tag::Active, p), Some(active(x, y)));
}
}
#[test]
fn page_list_point_from_pin_active_with_history() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
assert_eq!(
s.point_from_pin(Tag::Active, s.pin(screen(2, 30)).unwrap()),
Some(active(2, 0))
);
assert_eq!(
s.point_from_pin(Tag::Active, s.pin(screen(2, 21)).unwrap()),
None
);
}
#[test]
fn page_list_point_from_pin_active_from_prior_page() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
grow(&mut s, cap * 5);
let p = Pin {
node: s.last.unwrap(),
x: 2,
y: 0,
garbage: false,
};
assert_eq!(s.point_from_pin(Tag::Active, p), Some(active(2, 0)));
assert_eq!(
s.point_from_pin(Tag::Active, s.get_top_left(Tag::Screen).unwrap()),
None
);
}
#[test]
fn page_list_point_from_pin_traverse_pages() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
grow(&mut s, cap * 2);
let p = Pin {
node: s.node(s.last.unwrap()).unwrap().prev.unwrap(),
y: 5,
x: 2,
garbage: false,
};
assert_eq!(
s.point_from_pin(Tag::Screen, p),
Some(screen(2, (cap * (s.total_pages() - 2) + 5) as u32))
);
assert!(
s.point_from_pin(Tag::Active, s.get_top_left(Tag::Screen).unwrap())
.is_none()
);
}
#[test]
fn page_list_active_after_grow() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
assert_eq!(
s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()),
Some(screen(0, 10))
);
assert_eq!(s.get_bottom_right(Tag::Active).unwrap().y, 33);
finish(&s);
}
#[test]
fn page_list_scrollbar_with_max_size_0_after_grow() {
let mut s = PageList::init(80, 24, Some(0)).unwrap();
grow(&mut s, 10);
assert_eq!(
s.scrollbar(),
Scrollbar {
total: 24,
offset: 0,
len: 24
}
);
}
#[test]
fn page_list_scroll_with_max_size_0_no_history() {
let mut s = PageList::init(80, 24, Some(0)).unwrap();
grow(&mut s, 10);
let before = s.get_top_left(Tag::Viewport);
s.scroll(Scroll::DeltaRow(-5));
assert_eq!(s.viewport, Viewport::Active);
s.scroll(Scroll::Top);
assert_eq!(s.get_top_left(Tag::Viewport), before);
}
#[test]
fn page_list_scroll_top() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
assert_eq!(s.viewport_row_offset(), 10);
s.scroll(Scroll::Top);
assert_eq!(s.viewport_row_offset(), 0);
grow(&mut s, 10);
assert_eq!(
s.scrollbar(),
Scrollbar {
total: 44,
offset: 0,
len: 24
}
);
s.scroll(Scroll::Active);
assert_eq!(s.viewport_row_offset(), 20);
}
#[test]
fn page_list_scroll_delta_row_back() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::DeltaRow(-5));
assert_eq!(s.viewport_row_offset(), 5);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_delta_row_back_overflow() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::DeltaRow(-100));
assert_eq!(s.viewport_row_offset(), 0);
assert_eq!(s.viewport, Viewport::Top);
finish(&s);
}
#[test]
fn page_list_scroll_delta_row_forward() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::Top);
s.scroll(Scroll::DeltaRow(5));
assert_eq!(s.viewport_row_offset(), 5);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_delta_row_forward_into_active() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::Top);
s.scroll(Scroll::DeltaRow(100));
assert_eq!(s.viewport_row_offset(), 10);
assert_eq!(s.viewport, Viewport::Active);
finish(&s);
}
#[test]
fn page_list_scroll_to_pin() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::Pin(s.pin(screen(5, 3)).unwrap()));
assert_eq!(s.viewport_row_offset(), 3);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_to_pin_in_active() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::Pin(s.pin(active(5, 3)).unwrap()));
assert_eq!(s.viewport_row_offset(), 10);
assert_eq!(s.viewport, Viewport::Active);
finish(&s);
}
#[test]
fn page_list_scroll_to_pin_at_top() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 10);
s.scroll(Scroll::Pin(s.pin(screen(0, 0)).unwrap()));
assert_eq!(s.viewport_row_offset(), 0);
assert_eq!(s.viewport, Viewport::Top);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_0() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
s.scroll(Scroll::Row(0));
assert_eq!(s.viewport_row_offset(), 0);
assert_eq!(s.viewport, Viewport::Top);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_in_scrollback() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
s.scroll(Scroll::Row(10));
assert_eq!(s.viewport_row_offset(), 10);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_in_middle() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 50);
s.scroll(Scroll::Row(25));
assert_eq!(s.viewport_row_offset(), 25);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_at_active_boundary() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
s.scroll(Scroll::Row(30));
assert_eq!(s.viewport_row_offset(), 30);
assert_eq!(s.viewport, Viewport::Active);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_beyond_active() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
s.scroll(Scroll::Row(999));
assert_eq!(s.viewport_row_offset(), 30);
assert_eq!(s.viewport, Viewport::Active);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_without_scrollback() {
let mut s = PageList::init(80, 24, None).unwrap();
s.scroll(Scroll::Row(10));
assert_eq!(s.viewport_row_offset(), 0);
assert_eq!(s.viewport, Viewport::Active);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_then_delta() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
s.scroll(Scroll::Row(10));
s.scroll(Scroll::DeltaRow(5));
assert_eq!(s.viewport_row_offset(), 15);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_with_cache_fast_path_down() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 60);
s.scroll(Scroll::Row(10));
assert_eq!(s.scrollbar().offset, 10);
s.scroll(Scroll::Row(20));
assert_eq!(s.viewport_row_offset(), 20);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_scroll_to_row_with_cache_fast_path_up() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 60);
s.scroll(Scroll::Row(20));
assert_eq!(s.scrollbar().offset, 20);
s.scroll(Scroll::Row(10));
assert_eq!(s.viewport_row_offset(), 10);
assert_eq!(s.viewport, Viewport::Pin);
finish(&s);
}
#[test]
fn page_list_grow_fit_in_capacity() {
let mut s = PageList::init(80, 24, None).unwrap();
assert!(first_cap(&s) > 24);
assert_eq!(s.grow().unwrap(), None);
assert_eq!(
s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()),
Some(screen(0, 1))
);
}
#[test]
fn page_list_grow_allocate() {
let mut s = PageList::init(80, 24, None).unwrap();
let old = s.last.unwrap();
let cap = first_cap(&s);
grow(&mut s, cap - 24);
let new = s.grow().unwrap().unwrap();
assert_eq!(s.last, Some(new));
assert_eq!(s.node(old).unwrap().next, Some(new));
assert_eq!(s.pin(active(0, 23)).unwrap().node, new);
finish(&s);
}
#[test]
fn page_list_grow_prune_scrollback() {
let mut s = PageList::init(80, 24, Some(std_size())).unwrap();
let cap = first_cap(&s);
fill_pages(&mut s, 2);
let old = s.first.unwrap();
let second = s.node(old).unwrap().next.unwrap();
let bytes = s.page_size;
let h = s.track_pin(s.pin(screen(0, 0)).unwrap()).unwrap();
let y = cap / 2;
s.scroll(Scroll::Row(y));
assert_eq!(s.scrollbar().offset, y);
let new = s.grow().unwrap().unwrap();
assert_eq!(new.index(), old.index());
assert_ne!(new.generation(), old.generation());
assert_eq!(s.page_size, bytes);
assert_eq!(s.first, Some(second));
let p = s.tracked_pin(h).unwrap();
assert_eq!(p.node, second);
assert!(p.garbage);
assert_eq!(p.x, 0);
assert_eq!(p.y, 0);
assert_eq!(s.scrollbar().offset, y.saturating_sub(cap));
assert!(s.node(old).is_none());
finish(&s);
}
#[test]
fn page_list_grow_prune_scrollback_with_viewport_pin_not_in_pruned_page() {
let mut s = PageList::init(80, 24, Some(std_size())).unwrap();
let cap = first_cap(&s);
fill_pages(&mut s, 2);
let old = s.first.unwrap();
let second = s.node(old).unwrap().next.unwrap();
let bytes = s.page_size;
let h = s.track_pin(s.pin(screen(0, 0)).unwrap()).unwrap();
let y = cap + 5;
s.scroll(Scroll::Row(y));
assert_eq!(s.scrollbar().offset, y);
let new = s.grow().unwrap().unwrap();
assert_eq!(new.index(), old.index());
assert_ne!(new.generation(), old.generation());
assert_eq!(s.page_size, bytes);
assert_eq!(s.first, Some(second));
let p = s.tracked_pin(h).unwrap();
assert_eq!(p.node, second);
assert!(p.garbage);
assert_eq!(p.x, 0);
assert_eq!(p.y, 0);
assert_eq!(s.scrollbar().offset, y.saturating_sub(cap));
assert!(s.node(old).is_none());
finish(&s);
}
#[test]
fn page_list_increase_capacity_to_increase_styles() {
let mut s = PageList::init(80, 24, None).unwrap();
let old = s.first.unwrap();
let before = s.node(old).unwrap().data.capacity.styles;
let id = s
.increase_capacity(old, Some(IncreaseCapacity::Styles))
.unwrap();
assert_eq!(s.first, Some(id));
assert_eq!(s.node(id).unwrap().data.capacity.styles, before * 2);
assert_eq!(s.total_rows, 24);
finish(&s);
}
#[test]
fn page_list_increase_capacity_to_increase_graphemes() {
let mut s = PageList::init(80, 24, None).unwrap();
let old = s.first.unwrap();
let before = s.node(old).unwrap().data.capacity.grapheme_bytes;
let id = s
.increase_capacity(old, Some(IncreaseCapacity::GraphemeBytes))
.unwrap();
assert_eq!(s.first, Some(id));
assert_eq!(s.node(id).unwrap().data.capacity.grapheme_bytes, before * 2);
assert_eq!(s.total_rows, 24);
finish(&s);
}
#[test]
fn page_list_increase_capacity_to_increase_hyperlinks() {
let mut s = PageList::init(80, 24, None).unwrap();
let old = s.first.unwrap();
let before = s.node(old).unwrap().data.capacity.hyperlink_bytes;
let id = s
.increase_capacity(old, Some(IncreaseCapacity::HyperlinkBytes))
.unwrap();
assert_eq!(s.first, Some(id));
assert_eq!(
s.node(id).unwrap().data.capacity.hyperlink_bytes,
before * 2
);
assert_eq!(s.total_rows, 24);
finish(&s);
}
#[test]
fn page_list_increase_capacity_to_increase_string_bytes() {
let mut s = PageList::init(80, 24, None).unwrap();
let old = s.first.unwrap();
let before = s.node(old).unwrap().data.capacity.string_bytes;
let id = s
.increase_capacity(old, Some(IncreaseCapacity::StringBytes))
.unwrap();
assert_eq!(s.first, Some(id));
assert_eq!(s.node(id).unwrap().data.capacity.string_bytes, before * 2);
assert_eq!(s.total_rows, 24);
finish(&s);
}
#[test]
fn page_list_increase_capacity_tracked_pins() {
let mut s = PageList::init(80, 24, None).unwrap();
let h = s.track_pin(s.pin(active(3, 12)).unwrap()).unwrap();
let id = s
.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles))
.unwrap();
assert_eq!(s.tracked_pin(h).unwrap().node, id);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(3, 12))
);
finish(&s);
}
#[test]
fn page_list_increase_capacity_returns_out_of_space_at_max_capacity() {
let mut s = PageList::init(80, 24, None).unwrap();
let id = s.first.unwrap();
s.node_mut(id).unwrap().data.capacity.styles = u16::MAX;
assert_eq!(
s.increase_capacity(id, Some(IncreaseCapacity::Styles)),
Err(Error::OutOfSpace)
);
}
#[test]
fn page_list_increase_capacity_preserves_dirty_flag() {
let mut s = PageList::init(80, 24, None).unwrap();
s.node_mut(s.first.unwrap()).unwrap().data.dirty = true;
let id = s
.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles))
.unwrap();
assert!(s.node(id).unwrap().data.dirty);
finish(&s);
}
#[test]
fn page_list_page_iterator_single_page() {
let s = PageList::init(80, 24, None).unwrap();
let chunks: Vec<_> = s
.page_iterator(Direction::RightDown, active(0, 0), None)
.collect();
assert_eq!(chunks.len(), 1);
assert_eq!(
chunks[0],
Chunk {
node: s.first.unwrap(),
start: 0,
end: 24
}
);
assert!(chunks[0].full_page(&s));
}
#[test]
fn page_list_page_iterator_two_pages() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
grow(&mut s, cap - 12);
let chunks: Vec<_> = s
.page_iterator(Direction::RightDown, active(0, 0), None)
.collect();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].start, cap as u16 - 12);
assert_eq!(chunks[0].end, cap as u16);
assert_eq!(chunks[1].start, 0);
assert_eq!(chunks[1].end, 12);
}
#[test]
fn page_list_cell_iterator() {
let s = PageList::init(5, 3, None).unwrap();
let cells: Vec<_> = s
.cell_iterator(Direction::RightDown, screen(1, 0), Some(screen(3, 2)))
.collect();
assert_eq!(cells.len(), 13);
assert_eq!(cells.first().unwrap().x, 1);
assert_eq!(cells.last().unwrap().x, 3);
}
#[test]
fn page_list_page_iterator_reverse_single_page() {
let s = PageList::init(80, 24, None).unwrap();
let chunks: Vec<_> = s
.page_iterator(Direction::LeftUp, active(0, 0), None)
.collect();
assert_eq!(chunks.len(), 1);
assert_eq!(
chunks[0],
Chunk {
node: s.first.unwrap(),
start: 0,
end: 24
}
);
assert!(chunks[0].full_page(&s));
}
#[test]
fn page_list_page_iterator_reverse_two_pages() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
grow(&mut s, cap - 12);
let mut chunks: Vec<_> = s
.page_iterator(Direction::LeftUp, active(0, 0), None)
.collect();
chunks.reverse();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].start, cap as u16 - 12);
assert_eq!(chunks[0].end, cap as u16);
assert_eq!(chunks[1].start, 0);
assert_eq!(chunks[1].end, 12);
}
#[test]
fn page_list_cell_iterator_reverse() {
let s = PageList::init(5, 3, None).unwrap();
let cells: Vec<_> = s
.cell_iterator(Direction::LeftUp, screen(1, 0), Some(screen(3, 2)))
.collect();
assert_eq!(cells.len(), 13);
assert_eq!(cells.first().unwrap().x, 3);
assert_eq!(cells.last().unwrap().x, 1);
}
#[test]
fn page_list_resize_no_reflow_more_rows() {
let mut s = PageList::init(10, 3, None).unwrap();
grow(&mut s, 0);
let h = s.track_pin(s.pin(active(0, 2)).unwrap()).unwrap();
s.resize(Resize {
rows: Some(10),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.rows, 10);
assert_eq!(s.total_rows, 10);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(0, 2))
);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_more_rows_with_history() {
let mut s = PageList::init(10, 3, None).unwrap();
grow(&mut s, 50);
let h = s.track_pin(s.pin(active(0, 2)).unwrap()).unwrap();
s.resize(Resize {
rows: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.rows, 5);
assert_eq!(s.total_rows, 53);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(0, 4))
);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_rows() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
fill(&mut s);
s.resize(Resize {
rows: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.rows, 5);
assert_eq!(s.total_rows, 10);
assert_eq!(s.active_offset(), 10 - 5);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_one_rows() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
fill(&mut s);
s.resize(Resize {
rows: Some(1),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.rows, 1);
assert_eq!(s.total_rows, 10);
assert_eq!(s.active_offset(), 10 - 1);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_cols() {
let mut s = PageList::init(10, 3, None).unwrap();
fill(&mut s);
s.resize(Resize {
cols: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.cols, 5);
assert_eq!(s.total_rows, 3);
for n in s.nodes.iter().flatten() {
for y in 0..usize::from(n.data.size.rows) {
assert_eq!(n.data.get_cells(y).unwrap().len(), 5);
}
}
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_cols_pin_in_trimmed_cols() {
let mut s = PageList::init(10, 3, None).unwrap();
let h = s.track_pin(s.pin(active(9, 1)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(4, 1))
);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_more_cols() {
let mut s = PageList::init(5, 3, None).unwrap();
fill(&mut s);
s.resize(Resize {
cols: Some(10),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 3);
for y in 0..3 {
for x in 0..10 {
assert_eq!(
s.get_cell(screen(x, y)).unwrap().codepoint(),
if x < 5 { 65 } else { 0 }
);
}
}
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_cols_then_more_cols() {
let mut s = PageList::init(10, 3, None).unwrap();
fill(&mut s);
s.resize(Resize {
cols: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
s.resize(Resize {
cols: Some(10),
reflow: false,
..Resize::default()
})
.unwrap();
for y in 0..3 {
for x in 0..10 {
assert_eq!(
s.get_cell(screen(x, y)).unwrap().codepoint(),
if x < 5 { 65 } else { 0 }
);
}
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_more_cols_no_wrapped_rows() {
let mut s = PageList::init(5, 3, Some(0)).unwrap();
fill(&mut s);
s.resize(Resize {
cols: Some(10),
..Resize::default()
})
.unwrap();
assert_eq!(s.cols, 10);
assert_eq!(s.total_rows, 3);
for p in s.row_iterator(Direction::RightDown, screen(0, 0), None) {
assert_eq!(p.row_and_cell(&s).unwrap().1.codepoint(), 65);
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_more_cols_wrapped_rows() {
let mut s = PageList::init(2, 4, Some(0)).unwrap();
fill(&mut s);
wraps(&mut s);
s.resize(Resize {
cols: Some(4),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(s.active_offset(), 0);
assert_eq!(s.diagram(), "AAAA\nAAAA\n \n \n");
assert!(
!s.get_top_left(Tag::Screen)
.unwrap()
.row_and_cell(&s)
.unwrap()
.0
.wrap()
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_no_wrapped_rows() {
let mut s = PageList::init(10, 3, Some(0)).unwrap();
for y in 0..3 {
for x in 0..4 {
*s.get_cell_mut(screen(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
s.resize(Resize {
cols: Some(5),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 3);
for y in 0..3 {
for x in 0..4 {
assert_eq!(s.get_cell(screen(x, y)).unwrap().codepoint(), u32::from(x));
}
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_wrapped_rows() {
let mut s = PageList::init(4, 2, None).unwrap();
for y in 0..2 {
for x in 0..4 {
*s.get_cell_mut(screen(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(s.active_offset(), 2);
for y in 0..4 {
let p = s.pin(screen(0, y)).unwrap();
assert_eq!(p.row_and_cell(&s).unwrap().0.wrap(), y % 2 == 0);
assert_eq!(p.row_and_cell(&s).unwrap().1.codepoint(), (y % 2) * 2);
}
finish(&s);
}
#[test]
fn page_list_reset() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 0);
s.reset().unwrap();
assert_eq!(s.viewport, Viewport::Active);
assert_eq!(s.total_rows, 24);
assert_eq!(s.get_top_left(Tag::Active).unwrap().node, s.first.unwrap());
finish(&s);
}
#[test]
fn page_list_reset_across_two_pages() {
let mut s = PageList::init(1000, 100, None).unwrap();
grow(&mut s, 0);
s.reset().unwrap();
assert_eq!(s.viewport, Viewport::Active);
assert_eq!(s.total_rows, 100);
assert_eq!(s.get_top_left(Tag::Active).unwrap().node, s.first.unwrap());
finish(&s);
}
#[test]
fn page_list_clears_history() {
let mut s = PageList::init(80, 24, None).unwrap();
grow(&mut s, 30);
s.reset().unwrap();
assert_eq!(s.viewport, Viewport::Active);
assert_eq!(s.total_rows, 24);
assert_eq!(s.get_top_left(Tag::Active).unwrap().node, s.first.unwrap());
finish(&s);
}
#[test]
fn page_list_reset_moves_tracked_pins_and_marks_them_as_garbage() {
let mut s = PageList::init(80, 24, None).unwrap();
let h = s.track_pin(s.pin(active(42, 12)).unwrap()).unwrap();
s.reset().unwrap();
assert!(s.tracked_pin(h).unwrap().garbage);
assert!(!s.tracked_pin(s.viewport_pin.unwrap()).unwrap().garbage);
finish(&s);
}
#[test]
fn page_list_split_at_middle_row() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
write_rows(&mut s);
let p = s.pin(screen(0, 5)).unwrap();
s.split(p).unwrap();
assert_eq!(s.total_pages(), 2);
assert_eq!(s.node(s.first.unwrap()).unwrap().data.size.rows, 5);
for y in 0..10 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_split_at_row_0_is_no_op() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
write_rows(&mut s);
let p = s.pin(screen(0, 0)).unwrap();
s.split(p).unwrap();
assert_eq!(s.total_pages(), 1);
assert_eq!(s.node(s.first.unwrap()).unwrap().data.size.rows, 10);
for y in 0..10 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_split_at_last_row() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
write_rows(&mut s);
let p = s.pin(screen(0, 9)).unwrap();
s.split(p).unwrap();
assert_eq!(s.total_pages(), 2);
assert_eq!(s.node(s.first.unwrap()).unwrap().data.size.rows, 9);
for y in 0..10 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_split_single_row_page_returns_out_of_space() {
let mut s = PageList::init(10, 1, Some(0)).unwrap();
assert_eq!(
s.split(s.get_top_left(Tag::Active).unwrap()),
Err(Error::OutOfSpace)
);
}
#[test]
fn page_list_split_moves_tracked_pins() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
let h = s.track_pin(s.pin(screen(3, 7)).unwrap()).unwrap();
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
let p = s.tracked_pin(h).unwrap();
assert_eq!(p.node, s.last.unwrap());
assert_eq!(p.y, 2);
assert_eq!(p.x, 3);
finish(&s);
}
#[test]
fn page_list_split_tracked_pin_before_split_point_unchanged() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
let h = s.track_pin(s.pin(screen(5, 2)).unwrap()).unwrap();
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
let p = s.tracked_pin(h).unwrap();
assert_eq!(p.node, s.first.unwrap());
assert_eq!(p.y, 2);
assert_eq!(p.x, 5);
finish(&s);
}
#[test]
fn page_list_split_tracked_pin_at_split_point_moves_to_new_page() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
let h = s.track_pin(s.pin(screen(0, 5)).unwrap()).unwrap();
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
let p = s.tracked_pin(h).unwrap();
assert_eq!(p.node, s.last.unwrap());
assert_eq!(p.y, 0);
assert_eq!(p.x, 0);
finish(&s);
}
#[test]
fn page_list_split_multiple_tracked_pins_across_regions() {
let mut s = PageList::init(10, 10, None).unwrap();
let handles: Vec<_> = (0..10)
.map(|y| s.track_pin(s.pin(screen(y, y as u32)).unwrap()).unwrap())
.collect();
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
for (y, h) in handles.iter().enumerate() {
let p = s.tracked_pin(*h).unwrap();
assert_eq!(
s.point_from_pin(Tag::Screen, p),
Some(screen(y as u16, y as u32))
);
assert_eq!(
p.node,
if y < 5 {
s.first.unwrap()
} else {
s.last.unwrap()
}
);
}
finish(&s);
}
#[test]
fn page_list_split_tracked_viewport_pin_in_split_region_moves_correctly() {
let mut s = PageList::init(10, 3, None).unwrap();
grow(&mut s, 7);
s.scroll(Scroll::Row(6));
let before = s.scrollbar();
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
assert_eq!(s.scrollbar(), before);
assert_eq!(
s.tracked_pin(s.viewport_pin.unwrap()).unwrap().node,
s.last.unwrap()
);
finish(&s);
}
#[test]
fn page_list_split_preserves_styled_cells() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in 0..10 {
page.set_style(
0,
y,
crate::style::Style {
flags: crate::style::Flags {
bold: true,
..crate::style::Flags::default()
},
..crate::style::Style::default()
},
)
.unwrap();
}
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
for y in 0..10 {
let p = s.pin(screen(0, y)).unwrap();
assert!(p.style(&s).unwrap().flags.bold);
}
finish(&s);
}
#[test]
fn page_list_split_preserves_grapheme_clusters() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in 1..10 {
page.append_grapheme(0, y, 0x301).unwrap();
}
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
for y in 0..10 {
let p = s.pin(screen(0, y)).unwrap();
if y > 0 {
assert_eq!(p.grapheme(&s).unwrap(), [0x301]);
}
}
finish(&s);
}
#[test]
fn page_list_split_preserves_hyperlinks() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
let id = page
.insert_hyperlink(crate::hyperlink::Hyperlink {
id: crate::hyperlink::HyperlinkId::Implicit(1),
uri: b"https://example.com".to_vec(),
})
.unwrap();
for y in 0..10 {
if y > 0 {
page.hyperlink_set.r#use(id).unwrap();
}
page.set_hyperlink(0, y, id).unwrap();
}
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
for y in 0..10 {
let p = s.pin(screen(0, y)).unwrap();
assert!(p.row_and_cell(&s).unwrap().1.hyperlink());
}
finish(&s);
}
#[test]
fn page_list_split_preserves_wrap_flags() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in 0..10 {
page.rows[y].set_wrap(y % 2 == 0);
page.rows[y].set_wrap_continuation(y % 2 != 0);
}
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
for y in 0..10 {
let p = s.pin(screen(0, y)).unwrap();
assert_eq!(p.row_and_cell(&s).unwrap().0.wrap(), y % 2 == 0);
assert_eq!(
p.row_and_cell(&s).unwrap().0.wrap_continuation(),
y % 2 != 0
);
}
finish(&s);
}
#[test]
fn page_list_split_middle_page_preserves_linked_list_order() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let original = s.first;
s.split(s.pin(screen(0, 3)).unwrap()).unwrap();
s.split(s.pin(screen(0, 7)).unwrap()).unwrap();
s.split(s.pin(screen(0, 5)).unwrap()).unwrap();
assert_eq!(s.total_pages(), 4);
assert_eq!(s.first, original);
for y in 0..10 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_split_last_page_makes_new_page_the_last() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let original = s.first;
s.split(s.pin(screen(0, 3)).unwrap()).unwrap();
s.split(s.pin(screen(0, 7)).unwrap()).unwrap();
assert_eq!(s.total_pages(), 3);
assert_eq!(s.first, original);
for y in 0..10 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_split_first_page_keeps_original_as_first() {
let mut s = PageList::init(10, 10, None).unwrap();
write_rows(&mut s);
let original = s.first;
s.split(s.pin(screen(0, 7)).unwrap()).unwrap();
s.split(s.pin(screen(0, 3)).unwrap()).unwrap();
assert_eq!(s.total_pages(), 3);
assert_eq!(s.first, original);
for y in 0..10 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_compact_std_size_page_returns_null() {
let mut s = PageList::init(80, 24, None).unwrap();
assert_eq!(s.compact(s.first.unwrap()).unwrap(), None);
finish(&s);
}
#[test]
fn page_list_compact_oversized_page() {
let mut s = PageList::init(80, 24, None).unwrap();
write_rows(&mut s);
let h = s.track_pin(s.pin(screen(2, 10)).unwrap()).unwrap();
let id = s
.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::StringBytes))
.unwrap();
let bytes = s.page_size;
let compact = s.compact(id).unwrap().unwrap();
assert!(s.page_size < bytes);
assert_eq!(s.tracked_pin(h).unwrap().node, compact);
for y in 0..24 {
assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), y);
}
finish(&s);
}
#[test]
fn page_list_clone_partial_trimmed_right() {
let mut s = PageList::init(80, 20, None).unwrap();
grow(&mut s, 30);
let (q, _) = s.clone_range(screen(0, 0), Some(screen(0, 39))).unwrap();
assert_eq!(q.total_rows, 40);
finish(&q);
}
#[test]
fn page_list_clone_partial_trimmed_left() {
let mut s = PageList::init(80, 20, None).unwrap();
grow(&mut s, 30);
let (q, _) = s.clone_range(screen(0, 10), None).unwrap();
assert_eq!(q.total_rows, 40);
finish(&q);
}
#[test]
fn page_list_clone_partial_trimmed_both() {
let mut s = PageList::init(80, 20, None).unwrap();
grow(&mut s, 30);
let (q, _) = s.clone_range(screen(0, 10), Some(screen(0, 35))).unwrap();
assert_eq!(q.total_rows, 26);
finish(&q);
}
#[test]
fn page_list_clone() {
let s = PageList::init(80, 24, None).unwrap();
let q = s.try_clone().unwrap();
assert_eq!(q.total_rows, 24);
finish(&q);
}
#[test]
fn page_list_clone_less_than_active() {
let s = PageList::init(80, 24, None).unwrap();
let (q, _) = s.clone_range(active(0, 5), None).unwrap();
assert_eq!(q.total_rows, 24);
finish(&q);
}
#[test]
fn page_list_clone_remap_tracked_pin() {
let mut s = PageList::init(80, 24, None).unwrap();
let h = s.track_pin(s.pin(active(0, 6)).unwrap()).unwrap();
let (q, map) = s.clone_range(active(0, 5), None).unwrap();
assert_eq!(
q.point_from_pin(Tag::Active, q.tracked_pin(map[&h]).unwrap()),
Some(active(0, 1))
);
finish(&q);
}
#[test]
fn page_list_clone_remap_tracked_pin_not_in_cloned_area() {
let mut s = PageList::init(80, 24, None).unwrap();
let h = s.track_pin(s.pin(active(0, 3)).unwrap()).unwrap();
let (q, map) = s.clone_range(active(0, 5), None).unwrap();
assert!(!map.contains_key(&h));
finish(&q);
}
#[test]
fn page_list_clone_full_dirty() {
let mut s = PageList::init(80, 24, None).unwrap();
for y in [0, 12, 23] {
s.pin(active(0, y)).unwrap().mark_dirty(&mut s);
}
let q = s.try_clone().unwrap();
for y in 0..24 {
assert_eq!(q.is_dirty(active(0, y)), [0, 12, 23].contains(&y));
}
finish(&q);
}
#[test]
fn page_list_clone_partial_trimmed_left_reclaims_styles() {
let mut s = PageList::init(80, 20, None).unwrap();
grow(&mut s, 30);
let p = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in 0..10 {
p.set_style(
0,
y,
crate::style::Style {
flags: crate::style::Flags {
bold: true,
..Default::default()
},
..Default::default()
},
)
.unwrap();
}
let (q, _) = s.clone_range(screen(0, 10), None).unwrap();
assert_eq!(q.total_rows, 40);
assert_eq!(q.node(q.first.unwrap()).unwrap().data.styles.count(), 0);
finish(&q);
}
#[test]
fn page_list_erase() {
let mut s = PageList::init(80, 24, None).unwrap();
let bytes = s.page_size;
let cap = first_cap(&s);
grow(&mut s, cap * 5);
assert_eq!(s.total_pages(), 6);
let h = s.track_pin(s.pin(screen(0, 0)).unwrap()).unwrap();
s.erase_rows(Point::History(Coordinate::default()), None)
.unwrap();
assert_eq!(s.total_rows, 24);
assert_eq!(s.total_pages(), 1);
assert_eq!(s.page_size, bytes);
let pin = s.tracked_pin(h).unwrap();
assert_eq!(pin.node, s.first.unwrap());
assert_eq!((pin.x, pin.y), (0, 0));
finish(&s);
}
#[test]
fn page_list_erase_reaccounts_page_size() {
let mut s = PageList::init(80, 24, None).unwrap();
let bytes = s.page_size;
let cap = first_cap(&s);
grow(&mut s, cap * 5);
assert_eq!(s.total_pages(), 6);
let h = s.track_pin(s.pin(screen(0, 0)).unwrap()).unwrap();
s.erase_rows(Point::History(Coordinate::default()), None)
.unwrap();
assert_eq!(s.total_rows, 24);
assert_eq!(s.total_pages(), 1);
assert_eq!(s.page_size, bytes);
let pin = s.tracked_pin(h).unwrap();
assert_eq!(pin.node, s.first.unwrap());
assert_eq!((pin.x, pin.y), (0, 0));
finish(&s);
}
#[test]
fn page_list_erase_row_with_tracked_pin_resets_to_top_left() {
let mut s = PageList::init(80, 24, None).unwrap();
let bytes = s.page_size;
let cap = first_cap(&s);
grow(&mut s, cap * 5);
assert_eq!(s.total_pages(), 6);
let h = s.track_pin(s.pin(screen(0, 0)).unwrap()).unwrap();
s.erase_rows(Point::History(Coordinate::default()), None)
.unwrap();
assert_eq!(s.total_rows, 24);
assert_eq!(s.total_pages(), 1);
assert_eq!(s.page_size, bytes);
let pin = s.tracked_pin(h).unwrap();
assert_eq!(pin.node, s.first.unwrap());
assert_eq!((pin.x, pin.y), (0, 0));
finish(&s);
}
#[test]
fn page_list_erase_row_with_tracked_pin_shifts() {
let mut s = PageList::init(80, 24, None).unwrap();
let h = s.track_pin(s.pin(active(2, 4)).unwrap()).unwrap();
s.erase_rows(active(0, 0), Some(active(0, 3))).unwrap();
assert_eq!(s.total_rows, 24);
let p = s.tracked_pin(h).unwrap();
assert_eq!((p.x, p.y), (2, 0));
finish(&s);
}
#[test]
fn page_list_erase_row_with_tracked_pin_is_erased() {
let mut s = PageList::init(80, 24, None).unwrap();
let h = s.track_pin(s.pin(active(2, 2)).unwrap()).unwrap();
s.erase_rows(active(0, 0), Some(active(0, 3))).unwrap();
assert_eq!(s.total_rows, 24);
let p = s.tracked_pin(h).unwrap();
assert_eq!((p.x, p.y), (0, 0));
finish(&s);
}
#[test]
fn page_list_erase_active_regrows_automatically() {
let mut s = PageList::init(80, 24, None).unwrap();
s.erase_rows(active(0, 0), None).unwrap();
assert_eq!(s.total_rows, 24);
finish(&s);
}
#[test]
fn page_list_erase_a_one_row_active() {
let mut s = PageList::init(80, 1, None).unwrap();
s.erase_rows(active(0, 0), None).unwrap();
assert_eq!(s.total_rows, 1);
finish(&s);
}
#[test]
fn page_list_erase_row_bounded_less_than_full_row() {
let mut s = PageList::init(80, 10, None).unwrap();
let handles: Vec<_> = [5, 8, 9]
.into_iter()
.map(|y| s.track_pin(s.pin(active(0, y)).unwrap()).unwrap())
.collect();
s.erase_row_bounded(active(0, 5), 3).unwrap();
for (h, expected) in handles.iter().zip([4, 7, 9]) {
assert_eq!(s.tracked_pin(*h).unwrap().y, expected);
}
for y in 5..8 {
assert!(s.is_dirty(active(0, y)));
}
finish(&s);
}
#[test]
fn page_list_erase_row_bounded_with_pin_at_top() {
let mut s = PageList::init(80, 10, None).unwrap();
let h = s.track_pin(s.pin(active(5, 0)).unwrap()).unwrap();
s.erase_row_bounded(active(0, 0), 3).unwrap();
let p = s.tracked_pin(h).unwrap();
assert_eq!((p.x, p.y), (0, 0));
assert_eq!(s.total_rows, 10);
finish(&s);
}
#[test]
fn page_list_erase_row_bounded_full_rows_single_page() {
let mut s = PageList::init(80, 10, None).unwrap();
let a = s.track_pin(s.pin(active(0, 7)).unwrap()).unwrap();
let b = s.track_pin(s.pin(active(0, 9)).unwrap()).unwrap();
s.erase_row_bounded(active(0, 5), 10).unwrap();
assert_eq!(s.tracked_pin(a).unwrap().y, 6);
assert_eq!(s.tracked_pin(b).unwrap().y, 8);
for y in 5..10 {
assert!(s.is_dirty(active(0, y)));
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_cursor_in_unchanged_row() {
let mut s = PageList::init(4, 2, None).unwrap();
for y in 0..2 {
for x in 0..2 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
let h = s.track_pin(s.pin(active(1, 0)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(1, 0))
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_cursor_in_blank_cell() {
let mut s = PageList::init(6, 2, None).unwrap();
for y in 0..2 {
for x in 0..2 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
let h = s.track_pin(s.pin(active(2, 0)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(4),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(2, 0))
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_cursor_in_final_blank_cell() {
let mut s = PageList::init(6, 2, None).unwrap();
for y in 0..2 {
for x in 0..2 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
let h = s.track_pin(s.pin(active(3, 0)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(4),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(3, 0))
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_cursor_in_wrapped_blank_cell() {
let mut s = PageList::init(6, 2, None).unwrap();
for y in 0..2 {
for x in 0..2 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
let h = s.track_pin(s.pin(active(5, 0)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(4),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(3, 0))
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_cursor_in_wrapped_row() {
let mut s = PageList::init(4, 2, None).unwrap();
for y in 0..2 {
for x in 0..4 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
let h = s.track_pin(s.pin(active(2, 1)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(0, 1))
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_blank_lines() {
let mut s = PageList::init(4, 3, Some(0)).unwrap();
for y in [0, 0] {
for x in 0..4 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 3);
assert_eq!(s.get_cell(active(0, 1)).unwrap().codepoint(), 2);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_blank_lines_between() {
let mut s = PageList::init(4, 3, Some(0)).unwrap();
for y in [0, 2] {
for x in 0..4 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 5);
assert_eq!(s.get_cell(active(0, 1)).unwrap().codepoint(), 0);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_blank_lines_between_no_scrollback() {
let mut s = PageList::init(5, 3, Some(0)).unwrap();
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(65);
*s.get_cell_mut(active(0, 2)).unwrap() = Cell::init(67);
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 3);
assert_eq!(s.get_cell(active(0, 0)).unwrap().codepoint(), 65);
assert_eq!(s.get_cell(active(0, 1)).unwrap().codepoint(), 0);
assert_eq!(s.get_cell(active(0, 2)).unwrap().codepoint(), 67);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_cursor_not_on_last_line_preserves_location() {
let mut s = PageList::init(5, 5, Some(1)).unwrap();
for y in 0..5 {
for x in 0..2 {
*s.get_cell_mut(active(x, y)).unwrap() = Cell::init(u32::from(x));
}
}
grow(&mut s, 5);
let h = s.track_pin(s.pin(active(0, 0)).unwrap()).unwrap();
s.resize(Resize {
cols: Some(4),
cursor: Some(Cursor { x: 1, y: 1 }),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 10);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(0, 0))
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_no_reflow_preserves_semantic_prompt_on_first_line() {
let mut s = PageList::init(4, 4, Some(0)).unwrap();
s.node_mut(s.first.unwrap()).unwrap().data.rows[0]
.set_semantic_prompt(SemanticPrompt::Prompt);
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(
s.get_top_left(Tag::Active)
.unwrap()
.row_and_cell(&s)
.unwrap()
.0
.semantic_prompt(),
SemanticPrompt::Prompt
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_wrap_preserves_semantic_prompt() {
let mut s = PageList::init(4, 4, Some(0)).unwrap();
s.node_mut(s.first.unwrap()).unwrap().data.rows[0]
.set_semantic_prompt(SemanticPrompt::Prompt);
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(
s.get_top_left(Tag::Active)
.unwrap()
.row_and_cell(&s)
.unwrap()
.0
.semantic_prompt(),
SemanticPrompt::Prompt
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_no_reflow_preserves_semantic_prompt() {
let mut s = PageList::init(4, 4, Some(0)).unwrap();
s.node_mut(s.first.unwrap()).unwrap().data.rows[1]
.set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..4 {
*s.get_cell_mut(active(x, 1)).unwrap() = Cell::init(u32::from(x));
}
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
for y in [1, 2] {
assert_eq!(
s.pin(active(0, y))
.unwrap()
.row_and_cell(&s)
.unwrap()
.0
.semantic_prompt(),
SemanticPrompt::Prompt
);
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_to_wrap_a_wide_char() {
let mut s = PageList::init(3, 1, Some(0)).unwrap();
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(120);
let c = s.get_cell_mut(active(1, 0)).unwrap();
*c = Cell::init(0x1f600);
c.set_wide(Wide::Wide);
s.get_cell_mut(active(2, 0))
.unwrap()
.set_wide(Wide::SpacerTail);
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(s.get_cell(screen(1, 0)).unwrap().wide(), Wide::SpacerHead);
assert_eq!(s.get_cell(screen(0, 1)).unwrap().codepoint(), 0x1f600);
assert_eq!(s.get_cell(screen(1, 1)).unwrap().wide(), Wide::SpacerTail);
finish(&s);
}
#[test]
fn page_list_resize_reflow_more_cols_unwrap_still_requires_wide_spacer_head() {
let mut s = PageList::init(2, 2, Some(0)).unwrap();
for x in 0..2 {
*s.get_cell_mut(active(x, 0)).unwrap() = Cell::init(120);
}
wraps(&mut s);
let c = s.get_cell_mut(active(0, 1)).unwrap();
*c = Cell::init(0x1f600);
c.set_wide(Wide::Wide);
s.get_cell_mut(active(1, 1))
.unwrap()
.set_wide(Wide::SpacerTail);
s.resize(Resize {
cols: Some(3),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(s.get_cell(screen(2, 0)).unwrap().wide(), Wide::SpacerHead);
assert_eq!(s.get_cell(screen(0, 1)).unwrap().codepoint(), 0x1f600);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_to_wrap_a_multi_codepoint_grapheme_with_a_spacer_head() {
let mut s = PageList::init(4, 2, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
let cps = [0x200d, 0x1f468, 0x200d, 0x1f466, 0x200d, 0x1f466];
for x in [0, 2] {
page.cells[x] = Cell::init(0x1f468);
page.cells[x].set_wide(Wide::Wide);
page.cells[x + 1].set_wide(Wide::SpacerTail);
page.set_graphemes(x, 0, &cps).unwrap();
}
s.resize(Resize {
cols: Some(3),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
for y in 0..2 {
let p = s.pin(screen(0, y)).unwrap();
assert_eq!(p.row_and_cell(&s).unwrap().1.wide(), Wide::Wide);
assert_eq!(p.grapheme(&s).unwrap(), cps);
}
assert_eq!(s.get_cell(screen(2, 0)).unwrap().wide(), Wide::SpacerHead);
finish(&s);
}
#[test]
fn page_list_prompt_iterator_left_up() {
let mut s = PageList::init(2, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in [3, 6] {
page.rows[y].set_semantic_prompt(SemanticPrompt::Prompt);
}
for y in [7, 8, 12] {
page.rows[y].set_semantic_prompt(SemanticPrompt::PromptContinuation);
}
let ys: Vec<_> = s
.prompt_iterator(Direction::LeftUp, screen(0, 0), None)
.map(|p| s.screen_row(p).unwrap())
.collect();
assert_eq!(ys, [12, 6, 3]);
}
#[test]
fn page_list_prompt_iterator_right_down() {
let mut s = PageList::init(2, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in [3, 6] {
page.rows[y].set_semantic_prompt(SemanticPrompt::Prompt);
}
for y in [7, 8, 12] {
page.rows[y].set_semantic_prompt(SemanticPrompt::PromptContinuation);
}
let ys: Vec<_> = s
.prompt_iterator(Direction::RightDown, screen(0, 0), None)
.map(|p| s.screen_row(p).unwrap())
.collect();
assert_eq!(ys, [3, 6, 12]);
}
#[test]
fn page_list_highlight_semantic_content_prompt() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..5 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('A' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 5..8 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('B' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(2, 5)).unwrap(), SemanticContent::Prompt);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(0, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(7, 5)));
}
#[test]
fn page_list_highlight_semantic_content_prompt_with_output() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..3 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 3..7 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('l' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
for x in 7..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Prompt);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(0, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(6, 5)));
}
#[test]
fn page_list_highlight_semantic_content_prompt_multiline() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
}
{
for x in 0..5 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(2, 5)).unwrap(), SemanticContent::Prompt);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(0, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(4, 6)));
}
#[test]
fn page_list_highlight_semantic_content_prompt_only() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..5 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Prompt);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(0, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(4, 5)));
}
#[test]
fn page_list_highlight_semantic_content_prompt_to_end_of_screen() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 15;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..3 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 3..8 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 15)).unwrap(), SemanticContent::Prompt);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(0, 15)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(7, 15)));
}
#[test]
fn page_list_highlight_semantic_content_input_basic() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..3 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 3..8 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('l' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl = s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Input);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(3, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(7, 5)));
}
#[test]
fn page_list_highlight_semantic_content_input_with_output() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..5 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
for x in 5..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl = s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Input);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(2, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(4, 5)));
}
#[test]
fn page_list_highlight_semantic_content_input_multiline_with_continuation() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
for x in 0..2 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('>' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..6 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('d' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl = s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Input);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(2, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(5, 6)));
}
#[test]
fn page_list_highlight_semantic_content_input_no_input_returns_null() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..3 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 3..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl = s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Input);
assert!(hl.is_none());
}
#[test]
fn page_list_highlight_semantic_content_input_to_end_of_screen() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 15;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..7 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 15)).unwrap(), SemanticContent::Input);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(2, 15)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(6, 15)));
}
#[test]
fn page_list_highlight_semantic_content_input_prompt_only_returns_null() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
}
{
for y in 6..10 {
for x in 0..10 {
let cell = &mut page.get_cells_mut(y).unwrap()[x];
cell.set_semantic_content(SemanticContent::Prompt);
}
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl = s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Input);
assert!(hl.is_none());
}
#[test]
fn page_list_highlight_semantic_content_output_basic() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..5 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('l' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
for x in 5..8 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
for x in 8..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
cell.set_semantic_content(SemanticContent::Prompt);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Output);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(5, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(7, 5)));
}
#[test]
fn page_list_highlight_semantic_content_output_multiline() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..4 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('l' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
for x in 4..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
for x in 0..10 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
for x in 0..5 {
let cell = &mut page.get_cells_mut(7).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
for x in 5..10 {
let cell = &mut page.get_cells_mut(7).unwrap()[x];
cell.set_semantic_content(SemanticContent::Input);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Output);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(4, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(4, 7)));
}
#[test]
fn page_list_highlight_semantic_content_output_stops_at_next_prompt() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..4 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('l' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
for x in 4..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
for x in 0..3 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
for x in 3..6 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Output);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(4, 5)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(2, 6)));
}
#[test]
fn page_list_highlight_semantic_content_output_to_end_of_screen() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 15;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..2 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 2..4 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
for x in 4..10 {
let cell = &mut page.get_cells_mut(15).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
{
for x in 0..8 {
let cell = &mut page.get_cells_mut(16).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
for x in 8..10 {
let cell = &mut page.get_cells_mut(16).unwrap()[x];
cell.set_semantic_content(SemanticContent::Prompt);
}
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 15)).unwrap(), SemanticContent::Output);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(4, 15)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(7, 16)));
}
#[test]
fn page_list_highlight_semantic_content_output_no_output_returns_null() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..3 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
for x in 3..10 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('c' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
for y in 6..10 {
for x in 0..10 {
let cell = &mut page.get_cells_mut(y).unwrap()[x];
cell.set_semantic_content(SemanticContent::Input);
}
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Output);
assert!(hl.is_none());
}
#[test]
fn page_list_highlight_semantic_content_output_skips_empty_cells() {
let mut s = PageList::init(10, 20, Some(0)).unwrap();
let page = &mut s.node_mut(s.first.unwrap()).unwrap().data;
{
let rac_y = 5;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
for x in 0..3 {
let cell = &mut page.get_cells_mut(5).unwrap()[x];
*cell = Cell::init('$' as u32);
cell.set_semantic_content(SemanticContent::Prompt);
}
}
{
for x in 0..4 {
let cell = &mut page.get_cells_mut(6).unwrap()[x];
*cell = Cell::init('l' as u32);
cell.set_semantic_content(SemanticContent::Input);
}
}
{
for y in 7..9 {
for x in 0..5 {
let cell = &mut page.get_cells_mut(y).unwrap()[x];
*cell = Cell::init('o' as u32);
cell.set_semantic_content(SemanticContent::Output);
}
}
}
{
let rac_y = 10;
page.rows[rac_y].set_semantic_prompt(SemanticPrompt::Prompt);
}
let hl =
s.highlight_semantic_content(s.pin(screen(0, 5)).unwrap(), SemanticContent::Output);
let hl = hl.unwrap();
assert_eq!(s.point_from_pin(Tag::Screen, hl.0), Some(screen(0, 7)));
assert_eq!(s.point_from_pin(Tag::Screen, hl.1), Some(screen(4, 8)));
}
#[test]
fn page_list_resize_no_reflow_less_rows_trims_blank_lines() {
let mut s = PageList::init(10, 5, Some(0)).unwrap();
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(65);
let h = s.track_pin(s.pin(active(0, 0)).unwrap()).unwrap();
s.resize(Resize {
rows: Some(2),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 2);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(0, 0))
);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_rows_trims_blank_lines_cursor_in_blank_line() {
let mut s = PageList::init(10, 5, Some(0)).unwrap();
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(65);
let h = s.track_pin(s.pin(active(0, 3)).unwrap()).unwrap();
s.resize(Resize {
rows: Some(2),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(
s.point_from_pin(Tag::Active, s.tracked_pin(h).unwrap()),
Some(active(0, 1))
);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_rows_trims_blank_lines_erases_pages() {
let mut s = PageList::init(100, 5, Some(0)).unwrap();
let rows = first_cap(&s) as u16 + 5;
s.resize(Resize {
rows: Some(rows),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_pages(), 2);
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(65);
s.resize(Resize {
rows: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_pages(), 1);
assert_eq!(s.total_rows, 5);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_more_rows_extends_blank_lines() {
let mut s = PageList::init(10, 3, Some(0)).unwrap();
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(65);
s.resize(Resize {
rows: Some(7),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 7);
assert_eq!(s.active_offset(), 0);
assert_eq!(s.get_cell(active(0, 0)).unwrap().codepoint(), 65);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_cols_clears_graphemes() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
let p = &mut s.node_mut(s.first.unwrap()).unwrap().data;
p.cells[9] = Cell::init(9);
p.append_grapheme(9, 0, 10).unwrap();
assert_eq!(p.grapheme_count(), 1);
s.resize(Resize {
cols: Some(5),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 10);
for n in s.nodes.iter().flatten() {
assert_eq!(n.data.grapheme_count(), 0);
}
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_less_rows_and_cols() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
s.resize(Resize {
cols: Some(5),
rows: Some(7),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.cols, 5);
assert_eq!(s.rows, 7);
assert_eq!(s.total_rows, 7);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_more_rows_and_less_cols() {
let mut s = PageList::init(10, 10, Some(0)).unwrap();
s.resize(Resize {
cols: Some(5),
rows: Some(20),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.cols, 5);
assert_eq!(s.rows, 20);
assert_eq!(s.total_rows, 20);
finish(&s);
}
#[test]
fn page_list_resize_no_reflow_empty_screen() {
let mut s = PageList::init(5, 5, Some(0)).unwrap();
s.resize(Resize {
cols: Some(10),
rows: Some(10),
reflow: false,
..Resize::default()
})
.unwrap();
assert_eq!(s.cols, 10);
assert_eq!(s.rows, 10);
assert_eq!(s.total_rows, 10);
finish(&s);
}
#[test]
fn page_list_increase_capacity_after_col_shrink() {
let mut s = PageList::init(80, 24, None).unwrap();
s.resize(Resize {
cols: Some(40),
reflow: false,
..Resize::default()
})
.unwrap();
let id = s
.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles))
.unwrap();
assert_eq!(s.node(id).unwrap().data.size.cols, 40);
assert_eq!(s.node(id).unwrap().data.capacity.cols, 80);
finish(&s);
}
#[test]
fn page_list_increase_capacity_multi_page() {
let mut s = PageList::init(80, 24, None).unwrap();
fill_pages(&mut s, 2);
let old = s.first.unwrap();
let second = s.node(old).unwrap().next.unwrap();
let h = s
.track_pin(Pin {
node: old,
y: 2,
x: 3,
garbage: false,
})
.unwrap();
let id = s
.increase_capacity(old, Some(IncreaseCapacity::Styles))
.unwrap();
assert_eq!(s.node(id).unwrap().next, Some(second));
assert_eq!(s.node(second).unwrap().prev, Some(id));
assert_eq!(s.tracked_pin(h).unwrap().node, id);
finish(&s);
}
#[test]
fn page_list_compact_insufficient_savings_returns_null() {
let mut s = PageList::init(80, 24, Some(0)).unwrap();
let id = s
.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::GraphemeBytes))
.unwrap();
let bytes = s.page_size;
let result = s.compact(id).unwrap();
if result.is_some() {
assert!(s.page_size < bytes);
}
finish(&s);
}
#[test]
fn page_list_page_iterator_history_two_pages() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
fill_pages(&mut s, 2);
let chunks: Vec<_> = s
.page_iterator(
Direction::RightDown,
Point::History(Coordinate::default()),
None,
)
.collect();
assert_eq!(chunks.len(), 2);
assert_eq!((chunks[0].start, chunks[0].end), (0, cap as u16));
assert_eq!((chunks[1].start, chunks[1].end), (0, cap as u16 - 24));
}
#[test]
fn page_list_page_iterator_reverse_history_two_pages() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
fill_pages(&mut s, 2);
let mut chunks: Vec<_> = s
.page_iterator(
Direction::LeftUp,
Point::History(Coordinate::default()),
None,
)
.collect();
chunks.reverse();
assert_eq!(chunks.len(), 2);
assert_eq!((chunks[0].start, chunks[0].end), (0, cap as u16));
assert_eq!((chunks[1].start, chunks[1].end), (0, cap as u16 - 24));
}
#[test]
fn page_list_erase_row_bounded_full_rows_two_pages() {
let mut s = PageList::init(80, 24, None).unwrap();
let cap = first_cap(&s);
fill_pages(&mut s, 2);
write_rows(&mut s);
let h = s.track_pin(s.pin(screen(2, cap as u32)).unwrap()).unwrap();
let counts: Vec<_> = s.nodes.iter().flatten().map(|n| n.data.size.rows).collect();
s.erase_row_bounded(screen(0, cap as u32 - 2), 10).unwrap();
assert_eq!(
s.nodes
.iter()
.flatten()
.map(|n| n.data.size.rows)
.collect::<Vec<_>>(),
counts
);
assert_eq!(
s.point_from_pin(Tag::Screen, s.tracked_pin(h).unwrap()),
Some(screen(2, cap as u32 - 1))
);
assert_eq!(
s.get_cell(screen(0, cap as u32 - 2)).unwrap().codepoint(),
cap as u32 - 1
);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_wrapped_rows_with_graphemes() {
let mut s = PageList::init(4, 2, None).unwrap();
let p = &mut s.node_mut(s.first.unwrap()).unwrap().data;
for y in 0..2 {
for x in 0..4 {
let i = p.cell_index(x, y).unwrap();
p.cells[i] = Cell::init(x as u32 + 1);
p.append_grapheme(x, y, 10).unwrap();
}
}
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
for pin in s.cell_iterator(Direction::RightDown, screen(0, 0), None) {
assert_eq!(pin.grapheme(&s).unwrap(), [10]);
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_copy_style() {
let mut s = PageList::init(4, 2, None).unwrap();
fill(&mut s);
let p = &mut s.node_mut(s.first.unwrap()).unwrap().data;
let style = crate::style::Style {
fg_color: crate::style::Color::Palette(2),
..Default::default()
};
for y in 0..2 {
for x in 0..4 {
p.set_style(x, y, style).unwrap();
}
}
s.resize(Resize {
cols: Some(2),
..Resize::default()
})
.unwrap();
for pin in s.cell_iterator(Direction::RightDown, screen(0, 0), None) {
assert_eq!(pin.style(&s).unwrap(), style);
}
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_wraps_spacer_head() {
let mut s = PageList::init(4, 3, Some(0)).unwrap();
for x in 0..3 {
*s.get_cell_mut(active(x, 0)).unwrap() = Cell::init(120);
}
s.get_cell_mut(active(3, 0))
.unwrap()
.set_wide(Wide::SpacerHead);
s.node_mut(s.first.unwrap()).unwrap().data.rows[0].set_wrap(true);
s.node_mut(s.first.unwrap()).unwrap().data.rows[1].set_wrap_continuation(true);
let c = s.get_cell_mut(active(0, 1)).unwrap();
*c = Cell::init(0x1f600);
c.set_wide(Wide::Wide);
s.get_cell_mut(active(1, 1))
.unwrap()
.set_wide(Wide::SpacerTail);
s.resize(Resize {
cols: Some(3),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 3);
for x in 0..3 {
assert_eq!(s.get_cell(screen(x, 0)).unwrap().codepoint(), 120);
}
assert_eq!(s.get_cell(screen(0, 1)).unwrap().codepoint(), 0x1f600);
finish(&s);
}
#[test]
fn page_list_resize_reflow_more_cols_no_reflow_preserves_semantic_prompt() {
let mut s = PageList::init(4, 4, Some(0)).unwrap();
s.node_mut(s.first.unwrap()).unwrap().data.rows[0]
.set_semantic_prompt(SemanticPrompt::Prompt);
s.resize(Resize {
cols: Some(8),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 4);
assert_eq!(
s.get_top_left(Tag::Active)
.unwrap()
.row_and_cell(&s)
.unwrap()
.0
.semantic_prompt(),
SemanticPrompt::Prompt
);
finish(&s);
}
#[test]
fn page_list_scroll_clear() {
let mut s = PageList::init(80, 24, None).unwrap();
*s.get_cell_mut(active(0, 0)).unwrap() = Cell::init(65);
s.scroll_clear().unwrap();
assert_eq!(s.total_rows, 25);
assert_eq!(s.get_cell(active(0, 0)).unwrap().codepoint(), 0);
s.scroll_clear().unwrap();
assert_eq!(s.total_rows, 25);
finish(&s);
}
#[test]
fn page_list_resize_reflow_less_cols_to_eliminate_a_wide_char() {
let mut s = PageList::init(2, 1, Some(0)).unwrap();
let c = s.get_cell_mut(active(0, 0)).unwrap();
*c = Cell::init(0x1f600);
c.set_wide(Wide::Wide);
s.get_cell_mut(active(1, 0))
.unwrap()
.set_wide(Wide::SpacerTail);
s.resize(Resize {
cols: Some(1),
..Resize::default()
})
.unwrap();
assert_eq!(s.total_rows, 1);
assert_eq!(s.get_cell(active(0, 0)).unwrap(), Cell::default());
finish(&s);
}
#[test]
fn stale_tracked_handle_cannot_release_reused_slot() {
let mut s = PageList::init(4, 2, None).unwrap();
let p = s.get_top_left(Tag::Active).unwrap();
let old = s.track_pin(p).unwrap();
s.untrack_pin(old);
let new = s.track_pin(p).unwrap();
s.untrack_pin(old);
assert!(s.tracked_pin(old).is_none());
assert_eq!(s.tracked_pin(new), Some(p));
finish(&s);
}
#[test]
fn row_erasure_retains_page_dimensions_and_cursor_row() {
let mut s = PageList::init(80, 24, None).unwrap();
fill_pages(&mut s, 2);
write_rows(&mut s);
let p = s.pin(screen(3, 5)).unwrap();
let h = s.track_pin(p).unwrap();
let total = s.total_rows;
let cap = first_cap(&s);
s.erase_row(screen(0, 5)).unwrap();
assert_eq!(s.total_rows, total);
assert_eq!(first_cap(&s), cap);
assert_eq!(s.tracked_pin(h), Some(p));
assert_eq!(s.get_cell(screen(0, 5)).unwrap().codepoint(), 6);
finish(&s);
}
}
+65
View File
@@ -0,0 +1,65 @@
//! Coordinates relative to the active area, viewport, screen, or history.
/// Reference frame for a terminal coordinate.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Tag {
/// Editable bottommost screen area, including unwritten rows.
Active,
/// Visible area, including user scrolling.
Viewport,
/// Entire screen including scrollback.
Screen,
/// Scrollback preceding the active area.
History,
}
/// An x/y coordinate; history may exceed a single page's row count.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Coordinate {
/// Column.
pub x: u16,
/// Row relative to the point's reference frame.
pub y: u32,
}
impl Coordinate {
/// Compare both coordinates.
pub fn eql(self, other: Self) -> bool {
self == other
}
}
/// A coordinate with its reference frame.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Point {
/// Editable screen coordinate.
Active(Coordinate),
/// Visible viewport coordinate.
Viewport(Coordinate),
/// Whole-screen coordinate.
Screen(Coordinate),
/// Scrollback coordinate.
History(Coordinate),
}
impl Point {
/// Construct a point in a given frame.
pub const fn new(tag: Tag, coord: Coordinate) -> Self {
match tag {
Tag::Active => Self::Active(coord),
Tag::Viewport => Self::Viewport(coord),
Tag::Screen => Self::Screen(coord),
Tag::History => Self::History(coord),
}
}
/// Extract its coordinate.
pub const fn coord(self) -> Coordinate {
match self {
Self::Active(v) | Self::Viewport(v) | Self::Screen(v) | Self::History(v) => v,
}
}
/// Extract its reference frame.
pub const fn tag(self) -> Tag {
match self {
Self::Active(_) => Tag::Active,
Self::Viewport(_) => Tag::Viewport,
Self::Screen(_) => Tag::Screen,
Self::History(_) => Tag::History,
}
}
}
@@ -0,0 +1,252 @@
//! Bounded, deduplicated values with stable nonzero identifiers.
use std::{collections::HashMap, hash::Hash};
/// Errors callers can recover from by rebuilding or growing a page.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AddError {
/// The set has exhausted its logical capacity.
OutOfMemory,
/// Dead low IDs prevent allocating another sequential ID.
NeedsRehash,
/// Identifier does not refer to a live entry.
InvalidId,
/// A reference count would overflow or underflow.
InvalidRefCount,
}
impl std::fmt::Display for AddError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for AddError {}
/// Logical layout of the Zig set, independent of Rust collection overhead.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct Layout {
/// Slots including reserved ID zero.
pub cap: usize,
/// Power-of-two hash table capacity.
pub table_cap: usize,
/// Table byte offset.
pub table_start: usize,
/// Item byte offset.
pub items_start: usize,
/// Total byte requirement.
pub total_size: usize,
}
impl Layout {
/// Calculate a layout using the reference ABI's item size and alignment.
pub fn init(cap: usize, item_size: usize, item_align: usize) -> Self {
if cap == 0 {
return Self::default();
}
let table_cap = cap.next_power_of_two();
let items_start = (table_cap * 2).next_multiple_of(item_align);
let cap = table_cap * 13 / 16;
Self {
cap,
table_cap,
table_start: 0,
items_start,
total_size: items_start + cap * item_size,
}
}
}
#[derive(Debug, Clone)]
struct Item<T> {
value: T,
refs: u16,
}
/// A slot array and deduplication map. ID zero is always reserved.
#[derive(Debug, Clone)]
pub struct RefCountedSet<T> {
slots: Vec<Option<Item<T>>>,
lookup: HashMap<T, u16>,
next_id: usize,
living: usize,
}
impl<T: Eq + Hash + Clone> RefCountedSet<T> {
/// Construct with Zig's 13/16 load factor and power-of-two rounding.
pub fn init(cap: usize) -> Result<Self, AddError> {
if cap > 65536 {
return Err(AddError::OutOfMemory);
}
let slots_len = if cap == 0 {
0
} else {
cap.next_power_of_two() * 13 / 16
};
let mut slots = Vec::new();
slots
.try_reserve_exact(slots_len)
.map_err(|_| AddError::OutOfMemory)?;
slots.resize_with(slots_len, || None);
Ok(Self {
slots,
lookup: HashMap::new(),
next_id: 1,
living: 0,
})
}
/// Minimum table capacity for a given number of live values.
pub fn capacity_for_count(n: usize) -> usize {
if n == 0 {
0
} else {
((n + 1) * 16).div_ceil(13)
}
}
/// Maximum number of live values, excluding ID zero.
pub fn capacity(&self) -> usize {
self.slots.len().saturating_sub(1)
}
/// Number of values with nonzero reference counts.
pub fn count(&self) -> usize {
self.living
}
/// Look up a live value without retaining it.
pub fn lookup(&self, value: &T) -> Option<u16> {
self.lookup.get(value).copied()
}
/// Add or retain a value, trimming unused trailing IDs first.
pub fn add(&mut self, value: T) -> Result<u16, AddError> {
while self.next_id > 1
&& self.slots[self.next_id - 1]
.as_ref()
.is_none_or(|v| v.refs == 0)
{
self.next_id -= 1;
self.slots[self.next_id] = None;
}
if let Some(id) = self.lookup(&value) {
self.r#use(id)?;
return Ok(id);
}
if self.next_id >= self.slots.len() {
return Err(if self.living < self.slots.len() * 9 / 10 {
AddError::NeedsRehash
} else {
AddError::OutOfMemory
});
}
self.lookup
.try_reserve(1)
.map_err(|_| AddError::OutOfMemory)?;
let id = self.next_id as u16;
self.lookup.insert(value.clone(), id);
self.slots[self.next_id] = Some(Item { value, refs: 1 });
self.next_id += 1;
self.living += 1;
Ok(id)
}
/// Prefer an already allocated ID; return None when that ID was retained.
pub fn add_with_id(&mut self, value: T, id: u16) -> Result<Option<u16>, AddError> {
if id == 0 {
return Err(AddError::InvalidId);
}
if usize::from(id) < self.next_id {
if self.get(id) == Some(&value) {
self.r#use(id)?;
return Ok(None);
}
if self.ref_count(id) == 0 {
if let Some(other) = self.lookup(&value) {
self.r#use(other)?;
return Ok(Some(other));
}
self.lookup
.try_reserve(1)
.map_err(|_| AddError::OutOfMemory)?;
self.lookup.insert(value.clone(), id);
self.slots[usize::from(id)] = Some(Item { value, refs: 1 });
self.living += 1;
return Ok(None);
}
}
self.add(value).map(Some)
}
/// Retain a live value once.
pub fn r#use(&mut self, id: u16) -> Result<(), AddError> {
self.use_multiple(id, 1)
}
/// Retain a live value multiple times.
pub fn use_multiple(&mut self, id: u16, n: u16) -> Result<(), AddError> {
let item = self
.slots
.get_mut(usize::from(id))
.and_then(Option::as_mut)
.filter(|i| i.refs > 0)
.ok_or(AddError::InvalidId)?;
item.refs = item.refs.checked_add(n).ok_or(AddError::InvalidRefCount)?;
Ok(())
}
/// Obtain a live value without retaining it.
pub fn get(&self, id: u16) -> Option<&T> {
self.slots
.get(usize::from(id))?
.as_ref()
.filter(|i| i.refs > 0)
.map(|i| &i.value)
}
/// Release one reference. Dead values remain in their slots until reuse.
pub fn release(&mut self, id: u16) -> Result<(), AddError> {
self.release_multiple(id, 1)
}
/// Release several references.
pub fn release_multiple(&mut self, id: u16, n: u16) -> Result<(), AddError> {
let item = self
.slots
.get_mut(usize::from(id))
.and_then(Option::as_mut)
.filter(|i| i.refs > 0)
.ok_or(AddError::InvalidId)?;
item.refs = item.refs.checked_sub(n).ok_or(AddError::InvalidRefCount)?;
if item.refs == 0 {
self.lookup.remove(&item.value);
self.living -= 1;
}
Ok(())
}
/// Read the reference count, or zero for an absent ID.
pub fn ref_count(&self, id: u16) -> u16 {
self.slots
.get(usize::from(id))
.and_then(Option::as_ref)
.map_or(0, |i| i.refs)
}
pub(crate) fn contains_slot(&self, id: u16) -> bool {
self.slots.get(usize::from(id)).is_some_and(Option::is_some)
}
/// Iterate live IDs, values, and their reference counts.
pub fn entries(&self) -> impl Iterator<Item = (u16, &T, u16)> {
self.slots.iter().enumerate().filter_map(|(id, i)| {
i.as_ref()
.filter(|i| i.refs > 0)
.map(|i| (id as u16, &i.value, i.refs))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capacity_and_rehash() {
let mut s = RefCountedSet::init(16).unwrap();
let ids: Vec<_> = (0..12).map(|n| s.add(n).unwrap()).collect();
assert_eq!(s.add(99), Err(AddError::OutOfMemory));
s.release(ids[0]).unwrap();
s.release(ids[1]).unwrap();
assert_eq!(s.add(99), Err(AddError::NeedsRehash));
assert_eq!(s.add_with_id(99, ids[0]).unwrap(), None);
assert_eq!(s.count(), 11);
}
#[test]
fn trailing_reuse_and_dedup() {
let mut s = RefCountedSet::init(4).unwrap();
let a = s.add(1).unwrap();
let b = s.add(2).unwrap();
assert_eq!(s.add(1).unwrap(), a);
s.release(b).unwrap();
assert_eq!(s.add(3).unwrap(), b);
assert_eq!(s.ref_count(a), 2);
assert_eq!(s.r#use(0), Err(AddError::InvalidId));
}
}
+44
View File
@@ -0,0 +1,44 @@
//! Integer widths and safe logical offsets used by terminal pages.
use std::marker::PhantomData;
/// Maximum addressable page size.
pub const MAX_PAGE_SIZE: u32 = u32::MAX;
/// Byte offset within a page.
pub type OffsetInt = u32;
/// Number of cells in a grid dimension.
pub type CellCountInt = u16;
/// Style capacity and identifiers.
pub type StyleCountInt = u16;
/// Hyperlink capacity and identifiers.
pub type HyperlinkCountInt = u16;
/// Bytes of grapheme storage.
pub type GraphemeBytesInt = u32;
/// Bytes of string storage.
pub type StringBytesInt = u32;
/// A typed logical byte offset; never converted to a raw pointer.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Offset<T> {
offset: u32,
marker: PhantomData<fn() -> T>,
}
impl<T> Offset<T> {
/// Construct a logical byte offset.
pub const fn new(offset: u32) -> Self {
Self {
offset,
marker: PhantomData,
}
}
/// Return the byte displacement.
pub const fn offset(self) -> u32 {
self.offset
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offset() {
assert_eq!(std::mem::size_of::<Offset<u8>>(), 4);
}
}
+763
View File
@@ -0,0 +1,763 @@
//! Cell styles, color resolution, and VT/HTML serialization.
use crate::page::{Cell, ContentTag};
use ghostty_vt::{
color::{Palette, Rgb},
sgr::Underline,
};
use std::{
fmt,
hash::{Hash, Hasher},
};
/// Nonzero page-local style identifier.
pub type Id = u16;
/// The default style needs no set lookup.
pub const DEFAULT_ID: Id = 0;
/// A style color retains its palette or RGB origin.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Color {
/// Unspecified terminal default.
#[default]
None,
/// Index in the current palette.
Palette(u8),
/// Literal RGB value.
Rgb(Rgb),
}
impl Color {
/// Compare source and value.
pub fn eql(self, other: Self) -> bool {
self == other
}
/// Resolve a palette-dependent color.
pub fn resolve(self, palette: &Palette) -> Option<Rgb> {
match self {
Self::None => None,
Self::Palette(i) => Some(palette[usize::from(i)]),
Self::Rgb(v) => Some(v),
}
}
}
/// SGR boolean flags and underline kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Flags {
/// SGR bold attribute.
pub bold: bool,
/// SGR italic attribute.
pub italic: bool,
/// SGR faint attribute.
pub faint: bool,
/// SGR blink attribute.
pub blink: bool,
/// SGR inverse attribute.
pub inverse: bool,
/// SGR invisible attribute.
pub invisible: bool,
/// SGR strikethrough attribute.
pub strikethrough: bool,
/// SGR overline attribute.
pub overline: bool,
/// Underline decoration kind.
pub underline: Underline,
}
impl Default for Flags {
fn default() -> Self {
Self {
bold: false,
italic: false,
faint: false,
blink: false,
inverse: false,
invisible: false,
strikethrough: false,
overline: false,
underline: Underline::None,
}
}
}
impl Hash for Flags {
fn hash<H: Hasher>(&self, state: &mut H) {
self.bold.hash(state);
self.italic.hash(state);
self.faint.hash(state);
self.blink.hash(state);
self.inverse.hash(state);
self.invisible.hash(state);
self.strikethrough.hash(state);
self.overline.hash(state);
(self.underline as u8).hash(state);
}
}
/// Fully self-contained cell styling.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Style {
/// Foreground color.
pub fg_color: Color,
/// Background color.
pub bg_color: Color,
/// Underline color.
pub underline_color: Color,
/// SGR flags.
pub flags: Flags,
}
/// Bold foreground policy, independent of the configuration crate.
// TODO(integration): convert ghostty-config's BoldColor at the Screen boundary.
#[derive(Debug, Clone, Copy)]
pub enum BoldColor {
/// Use bright palette colors for the first eight entries.
Bright,
/// Override the default foreground when bold.
Color(Rgb),
}
/// Inputs needed to resolve foreground colors.
#[derive(Clone, Copy)]
pub struct Fg<'a> {
/// Terminal default foreground.
pub default: Rgb,
/// Current palette.
pub palette: &'a Palette,
/// Optional bold override.
pub bold: Option<BoldColor>,
}
impl Style {
/// True for default styling.
pub fn is_default(self) -> bool {
self == Self::default()
}
/// Compare all style fields.
pub fn eql(self, other: Self) -> bool {
self == other
}
/// Resolve background, including background-only cells.
pub fn bg(self, cell: &Cell, palette: &Palette) -> Option<Rgb> {
match cell.content_tag() {
ContentTag::BgColorPalette => Some(palette[usize::from(cell.color_palette())]),
ContentTag::BgColorRgb => Some(cell.color_rgb()),
_ => self.bg_color.resolve(palette),
}
}
/// Resolve foreground using bold palette and default overrides.
pub fn fg(self, opts: Fg<'_>) -> Rgb {
match self.fg_color {
Color::None => {
if self.flags.bold {
match opts.bold {
Some(BoldColor::Color(c)) => c,
_ => opts.default,
}
} else {
opts.default
}
}
Color::Palette(i) => {
opts.palette[usize::from(if self.flags.bold && opts.bold.is_some() && i < 8 {
i + 8
} else {
i
})]
}
Color::Rgb(c) => {
if self.flags.bold && c == opts.default {
match opts.bold {
Some(BoldColor::Color(v)) => v,
_ => c,
}
} else {
c
}
}
}
}
/// Resolve underline color.
pub fn underline_color(self, palette: &Palette) -> Option<Rgb> {
self.underline_color.resolve(palette)
}
/// Make a background-only cell, if a background is specified.
pub fn bg_cell(self) -> Option<Cell> {
let mut c = Cell::default();
match self.bg_color {
Color::None => return None,
Color::Palette(i) => {
c.set_content_tag(ContentTag::BgColorPalette);
c.set_color_palette(i);
}
Color::Rgb(v) => {
c.set_content_tag(ContentTag::BgColorRgb);
c.set_color_rgb(v);
}
}
Some(c)
}
/// Render self-contained SGR sequences, starting with reset.
pub fn formatter_vt(&self) -> VtFormatter<'_> {
VtFormatter {
style: self,
palette: None,
}
}
/// Render inline CSS declarations.
pub fn formatter_html(&self) -> HtmlFormatter<'_> {
HtmlFormatter {
style: self,
palette: None,
}
}
}
/// VT sequence formatter.
pub struct VtFormatter<'a> {
/// Style being formatted.
pub style: &'a Style,
/// Resolve palette entries to RGB when provided.
pub palette: Option<&'a Palette>,
}
impl fmt::Display for VtFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\x1b[0m")?;
if self.style.flags.bold {
f.write_str("\x1b[1m")?;
}
if self.style.flags.faint {
f.write_str("\x1b[2m")?;
}
if self.style.flags.italic {
f.write_str("\x1b[3m")?;
}
if self.style.flags.blink {
f.write_str("\x1b[5m")?;
}
if self.style.flags.inverse {
f.write_str("\x1b[7m")?;
}
if self.style.flags.invisible {
f.write_str("\x1b[8m")?;
}
if self.style.flags.strikethrough {
f.write_str("\x1b[9m")?;
}
if self.style.flags.overline {
f.write_str("\x1b[53m")?;
}
f.write_str(match self.style.flags.underline {
Underline::None => "",
Underline::Single => "\x1b[4m",
Underline::Double => "\x1b[4:2m",
Underline::Curly => "\x1b[4:3m",
Underline::Dotted => "\x1b[4:4m",
Underline::Dashed => "\x1b[4:5m",
})?;
for (prefix, mut c) in [
(38, self.style.fg_color),
(48, self.style.bg_color),
(58, self.style.underline_color),
] {
if let (Color::Palette(i), Some(p)) = (c, self.palette) {
c = Color::Rgb(p[usize::from(i)]);
}
match c {
Color::None => {}
Color::Palette(i) => write!(f, "\x1b[{prefix};5;{i}m")?,
Color::Rgb(v) => write!(f, "\x1b[{prefix};2;{};{};{}m", v.r, v.g, v.b)?,
}
}
Ok(())
}
}
/// Inline CSS formatter.
pub struct HtmlFormatter<'a> {
/// Style being formatted.
pub style: &'a Style,
/// Resolve palette entries to RGB when provided.
pub palette: Option<&'a Palette>,
}
impl fmt::Display for HtmlFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (prop, mut c) in [
("color", self.style.fg_color),
("background-color", self.style.bg_color),
("text-decoration-color", self.style.underline_color),
] {
if let (Color::Palette(i), Some(p)) = (c, self.palette) {
c = Color::Rgb(p[usize::from(i)]);
}
match c {
Color::None => {}
Color::Palette(i) => write!(f, "{prop}: var(--vt-palette-{i});")?,
Color::Rgb(v) => write!(f, "{prop}: rgb({}, {}, {});", v.r, v.g, v.b)?,
}
}
let flags = self.style.flags;
if flags.underline != Underline::None
|| flags.strikethrough
|| flags.overline
|| flags.blink
{
f.write_str("text-decoration-line:")?;
for (b, s) in [
(flags.underline != Underline::None, " underline"),
(flags.strikethrough, " line-through"),
(flags.overline, " overline"),
(flags.blink, " blink"),
] {
if b {
f.write_str(s)?;
}
}
f.write_str(";")?;
}
if flags.underline != Underline::None {
write!(
f,
"text-decoration-style: {};",
match flags.underline {
Underline::None | Underline::Single => "solid",
Underline::Double => "double",
Underline::Curly => "wavy",
Underline::Dotted => "dotted",
Underline::Dashed => "dashed",
}
)?;
}
for (b, s) in [
(flags.bold, "font-weight: bold;"),
(flags.italic, "font-style: italic;"),
(flags.faint, "opacity: 0.5;"),
(flags.invisible, "visibility: hidden;"),
(flags.inverse, "filter: invert(100%);"),
] {
if b {
f.write_str(s)?;
}
}
Ok(())
}
}
/// Page-local deduplicated style storage.
pub type Set = crate::ref_counted_set::RefCountedSet<Style>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_empty() {
let mut style = Style::default();
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_bold() {
let mut style = Style::default();
style.flags.bold = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[1m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_faint() {
let mut style = Style::default();
style.flags.faint = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[2m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_italic() {
let mut style = Style::default();
style.flags.italic = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[3m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_blink() {
let mut style = Style::default();
style.flags.blink = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[5m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_inverse() {
let mut style = Style::default();
style.flags.inverse = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[7m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_invisible() {
let mut style = Style::default();
style.flags.invisible = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[8m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_strikethrough() {
let mut style = Style::default();
style.flags.strikethrough = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[9m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_overline() {
let mut style = Style::default();
style.flags.overline = true;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[53m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_single() {
let mut style = Style::default();
style.flags.underline = Underline::Single;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[4m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_double() {
let mut style = Style::default();
style.flags.underline = Underline::Double;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[4:2m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_curly() {
let mut style = Style::default();
style.flags.underline = Underline::Curly;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[4:3m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_dotted() {
let mut style = Style::default();
style.flags.underline = Underline::Dotted;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[4:4m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_dashed() {
let mut style = Style::default();
style.flags.underline = Underline::Dashed;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[4:5m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_fg_palette() {
let mut style = Style::default();
style.fg_color = Color::Palette(42);
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[38;5;42m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_fg_rgb() {
let mut style = Style::default();
style.fg_color = Color::Rgb(Rgb {
r: 255,
g: 128,
b: 64,
});
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[38;2;255;128;64m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_bg_palette() {
let mut style = Style::default();
style.bg_color = Color::Palette(7);
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[48;5;7m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_bg_rgb() {
let mut style = Style::default();
style.bg_color = Color::Rgb(Rgb {
r: 32,
g: 64,
b: 96,
});
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[48;2;32;64;96m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_color_palette() {
let mut style = Style::default();
style.underline_color = Color::Palette(15);
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[58;5;15m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_underline_color_rgb() {
let mut style = Style::default();
style.underline_color = Color::Rgb(Rgb {
r: 200,
g: 100,
b: 50,
});
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[58;2;200;100;50m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_multiple_flags() {
let mut style = Style::default();
style.flags.bold = true;
style.flags.italic = true;
style.flags.underline = Underline::Single;
let mut formatter = style.formatter_vt();
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[1m\x1b[3m\x1b[4m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_all_flags() {
let mut style = Style::default();
style.flags.bold = true;
style.flags.italic = true;
style.flags.faint = true;
style.flags.blink = true;
style.flags.inverse = true;
style.flags.invisible = true;
style.flags.strikethrough = true;
style.flags.overline = true;
style.flags.underline = Underline::Curly;
let mut formatter = style.formatter_vt();
assert_eq!(
formatter.to_string(),
"\x1b[0m\x1b[1m\x1b[2m\x1b[3m\x1b[5m\x1b[7m\x1b[8m\x1b[9m\x1b[53m\x1b[4:3m"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_combined_colors_and_flags() {
let mut style = Style::default();
style.flags.bold = true;
style.flags.italic = true;
style.flags.underline = Underline::Double;
style.fg_color = Color::Rgb(Rgb { r: 255, g: 0, b: 0 });
style.bg_color = Color::Palette(8);
style.underline_color = Color::Rgb(Rgb { r: 0, g: 255, b: 0 });
let mut formatter = style.formatter_vt();
assert_eq!(
formatter.to_string(),
"\x1b[0m\x1b[1m\x1b[3m\x1b[4:2m\x1b[38;2;255;0;0m\x1b[48;5;8m\x1b[58;2;0;255;0m"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_all_colors_rgb() {
let mut style = Style::default();
style.fg_color = Color::Rgb(Rgb {
r: 10,
g: 20,
b: 30,
});
style.bg_color = Color::Rgb(Rgb {
r: 40,
g: 50,
b: 60,
});
style.underline_color = Color::Rgb(Rgb {
r: 70,
g: 80,
b: 90,
});
let mut formatter = style.formatter_vt();
assert_eq!(
formatter.to_string(),
"\x1b[0m\x1b[38;2;10;20;30m\x1b[48;2;40;50;60m\x1b[58;2;70;80;90m"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_all_colors_palette() {
let mut style = Style::default();
style.fg_color = Color::Palette(1);
style.bg_color = Color::Palette(2);
style.underline_color = Color::Palette(3);
let mut formatter = style.formatter_vt();
assert_eq!(
formatter.to_string(),
"\x1b[0m\x1b[38;5;1m\x1b[48;5;2m\x1b[58;5;3m"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_palette_with_palette_set_emits_rgb() {
let mut style = Style::default();
style.fg_color = Color::Palette(1);
let mut formatter = style.formatter_vt();
formatter.palette = Some(&ghostty_vt::color::DEFAULT);
assert_eq!(formatter.to_string(), "\x1b[0m\x1b[38;2;204;102;102m");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_vt_formatting_all_palette_colors_with_palette_set() {
let mut style = Style::default();
style.fg_color = Color::Palette(1);
style.bg_color = Color::Palette(2);
style.underline_color = Color::Palette(3);
let mut formatter = style.formatter_vt();
formatter.palette = Some(&ghostty_vt::color::DEFAULT);
assert_eq!(
formatter.to_string(),
"\x1b[0m\x1b[38;2;204;102;102m\x1b[48;2;181;189;104m\x1b[58;2;240;198;116m"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_basic_bold() {
let mut style = Style::default();
style.flags.bold = true;
let mut formatter = style.formatter_html();
assert_eq!(formatter.to_string(), "font-weight: bold;");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_fg_color_rgb() {
let mut style = Style::default();
style.fg_color = Color::Rgb(Rgb {
r: 255,
g: 128,
b: 64,
});
let mut formatter = style.formatter_html();
assert_eq!(formatter.to_string(), "color: rgb(255, 128, 64);");
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_bg_color_palette() {
let mut style = Style::default();
style.bg_color = Color::Palette(7);
let mut formatter = style.formatter_html();
assert_eq!(
formatter.to_string(),
"background-color: var(--vt-palette-7);"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_combined_colors_and_flags() {
let mut style = Style::default();
style.flags.bold = true;
style.flags.italic = true;
style.fg_color = Color::Rgb(Rgb { r: 255, g: 0, b: 0 });
style.bg_color = Color::Rgb(Rgb { r: 0, g: 0, b: 255 });
let mut formatter = style.formatter_html();
assert!(formatter.to_string().contains("color: rgb(255, 0, 0);"));
assert!(
formatter
.to_string()
.contains("background-color: rgb(0, 0, 255);")
);
assert!(formatter.to_string().contains("font-weight: bold;"));
assert!(formatter.to_string().contains("font-style: italic;"));
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_single_decoration_line() {
let mut style = Style::default();
style.flags.underline = Underline::Single;
let mut formatter = style.formatter_html();
assert!(
formatter
.to_string()
.contains("text-decoration-line: underline;")
);
assert!(
formatter
.to_string()
.contains("text-decoration-style: solid;")
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_multiple_decoration_lines() {
let mut style = Style::default();
style.flags.strikethrough = true;
style.flags.overline = true;
style.flags.underline = Underline::Curly;
let mut formatter = style.formatter_html();
assert!(
formatter
.to_string()
.contains("text-decoration-line: underline line-through overline;")
);
assert!(
formatter
.to_string()
.contains("text-decoration-style: wavy;")
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_palette_with_palette_set_emits_rgb() {
let mut style = Style::default();
style.bg_color = Color::Palette(7);
let mut formatter = style.formatter_html();
formatter.palette = Some(&ghostty_vt::color::DEFAULT);
assert_eq!(
formatter.to_string(),
"background-color: rgb(197, 200, 198);"
);
}
#[test]
#[allow(unused_mut, clippy::field_reassign_with_default)]
fn style_html_formatting_all_palette_colors_with_palette_set() {
let mut style = Style::default();
style.fg_color = Color::Palette(1);
style.bg_color = Color::Palette(2);
style.underline_color = Color::Palette(3);
let mut formatter = style.formatter_html();
formatter.palette = Some(&ghostty_vt::color::DEFAULT);
assert_eq!(
formatter.to_string(),
"color: rgb(204, 102, 102);background-color: rgb(181, 189, 104);text-decoration-color: rgb(240, 198, 116);"
);
}
#[test]
fn set_basic_usage() {
let mut set = Set::init(16).unwrap();
let a = Style {
flags: Flags {
bold: true,
..Flags::default()
},
..Style::default()
};
let b = Style {
flags: Flags {
italic: true,
..Flags::default()
},
..Style::default()
};
let id = set.add(a).unwrap();
assert!(id > 0);
assert_eq!(set.add(a).unwrap(), id);
assert!(set.get(id).unwrap().flags.bold);
let id2 = set.add(b).unwrap();
assert!(set.get(id2).unwrap().flags.italic);
assert_eq!(set.ref_count(id), 2);
assert_eq!(set.ref_count(id2), 1);
set.release(id).unwrap();
set.release(id2).unwrap();
assert_eq!(set.ref_count(id), 1);
assert_eq!(set.ref_count(id2), 0);
set.release(id).unwrap();
assert_eq!(set.ref_count(id), 0);
}
#[test]
fn set_capacities() {
assert!(Set::init(16384).is_ok());
}
}