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:
1 parent
09cbe3bc97
commit
6cf16ed2e6
11 files changed
+8817
-3
No files matched your search
@@ -6,8 +6,6 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ghostty-vt.workspace = true
|
||||
ghostty-unicode.workspace = true
|
||||
ghostty-datastruct.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user