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 59bc84fde0
commit b9ac54e794
11 files changed
+8817 -3

No files matched your search

Generated
+2
View File
@@ -872,6 +872,7 @@ dependencies = [
"ghostty-os", "ghostty-os",
"log", "log",
"ratatui", "ratatui",
"tempfile",
"thiserror 2.0.21", "thiserror 2.0.21",
] ]
@@ -916,6 +917,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"freetype-sys", "freetype-sys",
"ghostty-datastruct",
"ghostty-unicode", "ghostty-unicode",
"harfbuzz-sys", "harfbuzz-sys",
"log", "log",
-2
View File
@@ -6,8 +6,6 @@ license.workspace = true
[dependencies] [dependencies]
ghostty-vt.workspace = true ghostty-vt.workspace = true
ghostty-unicode.workspace = true
ghostty-datastruct.workspace = true
[lints] [lints]
workspace = true workspace = true
+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());
}
}
@@ -0,0 +1,304 @@
# ghostty-terminal page memory model — partial port
This is **not a completed Ghostty PageList parity port**. The core structures and
many operations are implemented, but the remaining PageList cases below must be
ported and used to drive corrections before Screen/Terminal relies on exact Zig
reflow, allocation, and viewport behavior. In particular, passing the current
crate tests does not establish equivalence of page boundaries under reflow.
No version bump or commit was made. Only `ghostty-vt` is required; no unicode,
datastruct, or config dependency was introduced. Reference files were read only.
## Module and test inventory
| Zig source | Rust module | Source cases represented | Extra Rust tests | Unported / skipped |
|---|---|---:|---:|---:|
| size.zig | size | 1 / 5 | 0 | 4 pointer-only tests skipped |
| point.zig | point | 0 / 0 | 0 | 0 |
| style.zig | style | 37 / 37 | 0 | 0 |
| hyperlink.zig | hyperlink | 0 / 0 | 0 | 0 |
| ref_counted_set.zig | ref_counted_set | 0 / 0 | 2 | 0 |
| page.zig | page | 46 / 46 | 0 | 0 |
| PageList.zig | page_list | 151 / 202 | 2 | 50 unported, 1 skipped |
Total: **239 passing Rust tests**, consisting of 235 source-named regression
cases and four added regressions. Some PageList cases use smaller equivalent
fixtures and consolidate their assertions; matching a test name is not a claim
that all incidental assertions or allocation paths in that Zig case were copied.
Style formatter fixtures retain their reference expected strings. All 17
semantic-highlight cases retain the reference grid setup and expected endpoints.
Intentional skips:
- `Offset ptr u8`, `Offset ptr structural`, `getOffset bytes`, `getOffset structs`:
raw-address/base-pointer machinery was replaced with owned storage and logical
cell indices. `Offset<T>` is a typed u32 logical offset; it does not dereference
page-relative pointers.
- `PageList init error`: Zig tripwire/allocator failure injection has no Rust
counterpart here. Fallible grid reservations return errors, but allocation
failure coverage is incomplete.
- `bitmap_allocator.zig` (21 tests) and `hash_map.zig` (22 tests) are not ported
as modules, as requested. Their logical accounting is implemented in page.rs.
## Implemented structures and behavior
- Cell and Row are transparent u64 wrappers, Copy + Clone, exactly eight bytes.
Cell preserves the Zig bit offsets: tag 0, payload 2, style ID 26, wide 42,
protected 44, hyperlink 45, semantic content 46. Row stores a cell index in its
low 32 bits and uses the same flag positions as Zig. Row cell indices are not
byte offsets; rotating rows moves metadata and preserves auxiliary-map keys.
- Page owns row/cell Vecs, a deduplicated style set, grapheme data and allocation
maps, a hyperlink map/set, and logical string/grapheme chunk allocators.
- `Page::layout` preserves the reference 64-bit Linux layout calculation,
including the unusual bitmap allocator formula `aligned_cap * chunk_size`,
bitmap rounding to 64 chunks, map headers even at zero capacity, set 13/16
load factor and reserved ID zero, and 4096-byte final rounding. A standalone
Zig ABI probe confirmed Style item size/alignment 36/4, hyperlink item 48/8,
and offset-slice 16/8. The layout does not report actual Rust heap overhead.
- Logical allocation tracks contiguous chunks, including temporary allocation
of a larger grapheme before freeing its old chunks. Grapheme and hyperlink
maps enforce separate entry limits. Dead hyperlink entries retain their
string chunks until their set slot is reclaimed.
- Page provides styles, graphemes, hyperlinks, copy/clone, partial-row copying,
moves, swaps, clearing, exact row capacity, and debug/test verification.
Unmanaged partial-row copies use a slice copy without per-cell maps or
allocation. Default styles bypass set lookup.
- PageList uses `Vec<Option<Node>>`, a free-index list, generation-bearing
NodeId, and a separate reusable standard-page pool. It accounts substandard
pages as standard pool allocations. Growing prunes whole history pages while
protecting the active rows. Reused node indices receive new generations.
- PageList has list-owned tracked handles, viewport scrolling, byte/row totals,
splitting, capacity growth, compaction, erasure, reset, history clearing,
cloning, row/column resize, reflow, prompt iteration/highlighting, and forward
or reverse page/row/cell iterators. Iterators also implement DoubleEndedIterator
where appropriate. PromptIterator follows continuation handling from Zig.
## Important deviations / remaining work
1. **Reflow and wider no-reflow resize rebuild a snapshot of rows into new pages.**
They do not yet implement Zig's incremental ReflowCursor and page backfill
algorithm. New page auxiliary capacities are sized from encountered content,
instead of following the exact sequence of capacity doubling and splitting.
Page boundaries, intermediate allocations, pruning, and maximum-size behavior
can differ. This is the most important unfinished work.
2. Snapshot reflow is linear in all cells and allocates temporary row vectors,
copies managed values, and scans tracked locations while copying. It is not
ready for the intended terminal hot-path performance requirements. Point
conversion and viewport offsets traverse the list; the Zig active/viewport
caches have not been reproduced.
3. Cursor preservation during combined row/column resize is incomplete. Basic
tracked cursor positions, blank-cell clamping, trailing blank trimming, and
several wide/spacer cases are tested, but cross-page cursors, old/new wrap
counts, viewport remapping under reflow, and all combinations need the
remaining reference cases.
4. The HashMap-backed RefCountedSet preserves zero reservation, bounded live
slots, sequential IDs, trailing dead-slot trimming, `add_with_id`, checked
reference counts, and OutOfMemory/NeedsRehash errors. It does **not** reproduce
Robin Hood probe-dependent dead-ID reuse or the 32-probe failure threshold.
Thus exact reused IDs and the point at which NeedsRehash occurs can differ.
Hash values are Rust implementation details; Zig Style/Wyhash output is not
reproduced. Style implements Hash/Eq for the Rust deduplication map.
5. Direct Page clone uses Rust Clone. Some auxiliary temporary allocations and
owned-value clones still use infallible standard collection operations.
Actual process allocator exhaustion is not completely surfaced as Result;
logical capacity exhaustion is. Allocation-failure atomicity has not been
fully audited. There is no unsafe code or todo!/unimplemented! path.
6. The row-range eraser supports arbitrary start ranges, while Zig's tested
eraseRows use is primarily prefix/history/active erasure. Exact destination
pins for arbitrary whole-page removal in the middle need further parity work.
7. `style::BoldColor` is a small local policy enum because terminal must not
depend on config. TODO(integration): convert the configuration type at the
Screen boundary. Rgb/Palette/Underline are reused from ghostty-vt.
8. Logical layout is for 64-bit Linux with a 4096-byte minimum page size. Other
ABIs are not implemented. Pool reuse reuses Pages with matching capacities;
it does not share a mmap allocator between independent PageLists.
## API overview for Screen / Terminal agents
### Coordinate and lifetime model
`Point::{Active,Viewport,Screen,History}(Coordinate { x: u16, y: u32 })`
selects the coordinate frame. `Point::new(Tag, Coordinate)` is useful for dynamic
frames. `pin(point)` and `point_from_pin(tag, pin)` return Option for bounds errors.
`get_top_left` and `get_bottom_right` return None for an empty region such as
history before the first scroll.
`Pin { node: NodeId, y: u16, x: u16, garbage: bool }` is Copy and contains no
references. Pass the owning list explicitly to its access and movement methods:
`p.row_and_cell(&list)`, `p.style(&list)`, `p.grapheme(&list)`,
`p.cells(&list, CellSubset::All)`, `p.up(&list, n)`, `p.right_wrap(&list, n)`, etc.
`CellSubset::Left` and `Right` both include the pin column. Vertical overflow
methods return `Overflow::{Offset,Overflow { end, remaining }}`. `diagonal` uses
signed deltas and clamps columns. `before`/`is_between` use row-major order.
Untracked pins must be reacquired after content-moving mutations. For a cursor,
selection endpoint, or saved location, call `track_pin(pin) -> Result<TrackedPin>`.
Read its current Copy value using `tracked_pin(handle)`; change its position with
`set_tracked_pin(handle, pin)`. Release with `untrack_pin(handle)`. Handles are
specific to the owning list. Recycled handles have a new generation, so releasing
an old handle does not release its successor. Pruning/reset mark external pins
as garbage; ordinary erasure remaps them without setting garbage. The list owns
one internal tracked viewport handle, included in `count_tracked_pins()`.
### Reading and writing cells
`PageList::get_cell(point)` returns a Copy Cell, and `get_cell_mut(point)` returns
a direct mutable reference. `node(id)` / `node_mut(id)` expose the page.
`Page::get_row(y)`, `get_cells(y)`, `get_cells_mut(y)`, and `cell_index(x,y)` use
logical row coordinates and preserve O(1) row access even after row rotation.
Auxiliary lookup methods take a **cell index** returned by cell_index, not a
column or pointer. The vectors include unused capacity; iteration should respect
`page.size`, not the vector lengths.
Construct plain text with `Cell::init(codepoint)`. Set flags with `set_wide`,
`set_protected`, `set_semantic_content`, etc. `set_codepoint` changes only the
payload; use `set_content_tag` when changing between text and background colors.
For managed cells, clear the old data with page operations before assigning a
new raw Cell. Direct writes must maintain row flags and dirty tracking.
- `set_style(x,y,Style)` handles style add/release and marks the row styled.
Default style is ID 0; `page.style(cell)` resolves it without map access.
- `append_grapheme(x,y,cp)` and `set_graphemes(x,y,&extra)` store only codepoints
beyond the cell's first codepoint. Set the first nonzero codepoint beforehand.
- `insert_hyperlink(Hyperlink)` returns an already-retained ID. Attach it with
`set_hyperlink(x,y,id)`, which consumes that reference without incrementing it.
For another cell, call `page.hyperlink_set.r#use(id)` first. If attachment fails,
release the retained ID. `clear_hyperlink(index)` releases the cell reference.
- `clear_cells(y,start,end,protect)` uses half-open columns. `protect=true` is a
Rust convenience that skips protected cells. Move and swap helpers transfer
auxiliary ownership and avoid changing reference counts for moved values.
`row.set_dirty(true)` / `pin.mark_dirty(&mut list)` support direct writes;
`list.clear_dirty()` resets page and row flags. `is_dirty` checks both levels.
Conservative managed-row flags may be true even after their last value is cleared.
Use the explicit `update_row_*_flag` methods when a precise flag is needed.
### Errors and capacity recovery
`page::Error` includes separate style set, grapheme allocator/map, hyperlink
set/map, and string exhaustion variants. `ref_counted_set::AddError` distinguishes
OutOfMemory from NeedsRehash, plus invalid IDs/counts for safe API misuse handling.
`RefCountedSet::add_with_id(value,id)` returns None when the requested ID was used,
otherwise Some(actual_id). `get` and `lookup` do not retain values.
`increase_capacity(node, Some(IncreaseCapacity::{Styles,GraphemeBytes,
HyperlinkBytes,StringBytes}))` replaces a page with increased limits and remaps
tracked pins; None rebuilds the same capacity to reclaim dead IDs.
`adjust_capacity` is an alias. OutOfSpace means doubling/page-size limits prevent
further growth; `split(pin)` can divide at a row boundary. Splitting row zero is
otherwise a no-op, and a single-row page returns OutOfSpace. `compact(node)`
returns Some(replacement) only when an oversized page can shrink.
Always use the returned NodeId, or resolve a tracked pin again after replacement.
### List mutations and iteration
`grow()` adds one row, returning Some(new_node) if another page was needed.
`scroll(Scroll::{Active,Top,Row,DeltaRow,DeltaPrompt,Pin})` only changes viewport.
`scrollbar()` reports row counts and hides physical history at max_size zero.
`scroll_clear()` moves the nonempty active rows into history.
`erase_rows(top, optional_bottom)` removes inclusive rows and ensures active height.
`erase_row(point)` shifts the remainder of the screen while retaining page sizes.
`erase_row_bounded(point,limit)` shifts at most limit following rows.
`erase_cells_bounded(start_pin,end_pin,left,right)` is an extra rectangular helper.
`reset()` clears content and invalidates tracked contents; `clear_history()`
preserves the active area.
`resize(Resize { cols, rows, reflow, cursor })` takes optional new dimensions and
an optional active `Cursor { x,y }`. Reflow defaults to true. Cursor is input
context; track the actual cursor pin separately to read back its new position.
Consult the limitations above before relying on complex resize behavior.
`clone_range(top,bottom)` clones inclusive **whole rows**, ignoring x, pads to the
active height, and returns `(PageList, HashMap<old_handle,new_handle>)`.
`try_clone()` clones the whole screen without retaining external tracked pins.
Clone viewport defaults to Active; dirty information is retained by rows.
Page itself implements Clone for an independent full copy; its inherent
`clone_from(source,start,end)` copies a half-open row range into existing capacity.
`clone_partial_row_from` accepts source/destination row and column plus length;
`clone_cells_within` handles same-page copies without conflicting borrows.
`page_iterator`, `row_iterator`, `cell_iterator` take Direction, start Point,
and optional inclusive end Point. End None uses the frame's bottom-right.
Reverse direction traverses the same bounds backwards. PageIterator yields
`Chunk { node,start,end }` with half-open page-local rows. Iterators borrow the
list immutably; collect Copy pins/chunks or end iteration before mutating it.
`prompt_iterator` finds semantic prompt starts; `highlight_semantic_content`
returns an optional inclusive `(start_pin,end_pin)` for a prompt's Prompt, Input,
or Output region.
`Page::verify_integrity` and `PageList::verify_integrity` are available in debug
and test builds, as are assert_integrity helpers. Nestable pause methods support
batched low-level updates. Verification is explicit; raw mutable access can
break invariants until the caller completes its update.
## Remaining PageList source tests
The following 50 cases are **unported**, rather than justified skips. They include
important production behavior and should drive the next implementation pass.
- `PageList grow allows exceeding max size for active area`
- `PageList grow prune required with a single page`
- `PageList scroll delta row back without space preserves active`
- `PageList: jump zero prompts`
- `Screen: jump back one prompt`
- `Screen: jump forward prompt skips multiline continuation`
- `PageList eraseRows invalidates viewport offset cache`
- `PageList eraseRow invalidates viewport offset cache`
- `PageList eraseRowBounded invalidates viewport offset cache`
- `PageList eraseRowBounded multi-page invalidates viewport offset cache`
- `PageList eraseRowBounded full page shift invalidates viewport offset cache`
- `PageList eraseRowBounded exhausts pages invalidates viewport offset cache`
- `PageList promptIterator right_down continuation at start`
- `PageList promptIterator right_down with prompt before continuation`
- `PageList promptIterator right_down limit inclusive`
- `PageList promptIterator left_up limit inclusive`
- `PageList erase resets viewport to active if moves within active`
- `PageList erase resets viewport if inside erased page but not active`
- `PageList erase resets viewport to active if top is inside active`
- `PageList resize (no reflow) less rows cursor on bottom`
- `PageList resize (no reflow) less rows cursor in scrollback`
- `PageList resize (no reflow) more rows contains viewport`
- `PageList resize (no reflow) more cols with spacer head`
- `PageList resize (no reflow) grow cols fast path with spacer head`
- `PageList resize (no reflow) more cols forces less rows per page`
- `PageList resize more rows and cols doesn't fit in single std page`
- `PageList resize (no reflow) more cols forces smaller cap`
- `PageList resize (no reflow) more rows adds blank rows if cursor at bottom`
- `PageList resize reflow invalidates viewport offset cache`
- `PageList resize reflow more cols creates multiple pages`
- `PageList resize reflow more cols wrap across page boundary`
- `PageList resize reflow more cols wrap across page boundary cursor in second page`
- `PageList resize reflow less cols wrap across page boundary cursor in second page`
- `PageList resize reflow more cols cursor in wrapped row`
- `PageList resize reflow more cols cursor in not wrapped row`
- `PageList resize reflow more cols cursor in wrapped row that isn't unwrapped`
- `PageList resize reflow exceeds hyperlink memory forcing capacity increase`
- `PageList resize reflow exceeds grapheme memory forcing capacity increase`
- `PageList resize reflow exceeds style memory forcing capacity increase`
- `PageList resize reflow more cols unwrap wide spacer head`
- `PageList resize reflow more cols unwrap wide spacer head across two rows`
- `PageList resize reflow less cols cursor goes to scrollback`
- `PageList resize reflow less cols copy kitty placeholder`
- `PageList resize reflow more cols clears kitty placeholder`
- `PageList resize reflow wrap moves kitty placeholder`
- `PageList resize reflow grapheme map capacity exceeded`
- `PageList resize grow cols with unwrap fixes viewport pin`
- `PageList grow reuses non-standard page without leak`
- `PageList grow non-standard page prune protection`
- `PageList resize (no reflow) more cols remaps pins in backfill path`
## Quality gate
Final gate (2026-10-11):
- `cargo fmt -p ghostty-terminal`: passed; only the documented nightly-option
warnings from workspace rustfmt configuration.
- `cargo build -p ghostty-terminal`: passed.
- `cargo test -p ghostty-terminal --quiet`: 239 passed, 0 failed, 0 ignored.
- `cargo clippy -p ghostty-terminal --all-targets -- -D warnings`: passed.
- `git diff --check -- crates/ghostty-terminal docs/rewrite/notes/ghostty-terminal-pagelist.md`:
passed for tracked changes.
The early missing `ghostty-vt::stream` dependency file was resolved by concurrent
workspace work; no change to that crate was needed here. The terminal Cargo.toml
already had local changes at the beginning of this task and was not edited here.