//! Linked terminal pages, byte-bounded history, and stable tracked coordinates. use std::{ collections::HashMap, sync::atomic::{AtomicUsize, Ordering}, }; use crate::{ page::{ self, Adjustment, Capacity, Cell, ContentTag, Error, Page, Row, SemanticContent, SemanticPrompt, Wide, }, point::{Coordinate, Point, Tag}, }; /// 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, /// Next page. pub next: Option, /// 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, } /// 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, /// New row count, or unchanged. pub rows: Option, /// Rewrap logical lines when changing columns. pub reflow: bool, /// Cursor position whose surrounding rows should be preserved. pub cursor: Option, } 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>, free_nodes: Vec, page_pool: Vec, /// Never-written pages (all cells zero) that need no `reinit` memset. clean_pool: Vec, /// Oldest page. pub first: Option, /// Last active page. pub last: Option, /// 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, free_pins: Vec, viewport_pin: Option, /// Absolute viewport offset, or usize::MAX when invalidated. Atomic interior /// mutability preserves the read-only coordinate API and Send/Sync traits. viewport_pin_row_offset: AtomicUsize, pin_serial: u64, pause_checks: usize, } /// Failure points of [`PageList::init`] (Zig: `init_tw`). The Zig /// `init_memory_pool` point has no counterpart: Rust has no up-front memory /// pool, the slabs start empty and grow with the fallible reserves below. /// `viewport_pin` and `viewport_pin_track` are two allocations in Zig (pool /// create + set insert) that are one reserve in `track_pin` here; both points /// are checked before it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum InitPoint { InitPages, ViewportPin, ViewportPinTrack, } crate::tripwire_module!(init_tw, InitPoint, Error); /// Failure points of the page creation done by `init` (Zig: `initPages_tw`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum InitPagesPoint { Node, BufStd, BufNonStd, } crate::tripwire_module!(init_pages_tw, InitPagesPoint, Error); 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) -> Result { if cols == 0 || rows == 0 { return Err(Error::InvalidDimensions); } let mut s = Self { nodes: Vec::new(), free_nodes: Vec::new(), page_pool: Vec::new(), clean_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, viewport_pin_row_offset: AtomicUsize::new(usize::MAX), pin_serial: 0, pause_checks: 0, }; init_tw::check(InitPoint::InitPages)?; while s.total_rows < usize::from(rows) { s.grow()?; } let pin = s .get_top_left(Tag::Screen) .ok_or(Error::InvalidCoordinate)?; init_tw::check(InitPoint::ViewportPin)?; init_tw::check(InitPoint::ViewportPinTrack)?; 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 { if Page::layout(cap).total_size > u32::MAX as usize { return Err(Error::OutOfSpace); } init_pages_tw::check(InitPagesPoint::Node)?; self.nodes.try_reserve(1).map_err(|_| Error::OutOfMemory)?; let mut data = if let Some(mut p) = self.clean_pool.pop().filter(|p| p.capacity == cap) { p.size = page::Size { cols: cap.cols, rows: cap.rows, }; p } else 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 { if Page::layout(cap).total_size <= std_size() { init_pages_tw::check(InitPagesPoint::BufStd)?; } else { init_pages_tw::check(InitPagesPoint::BufNonStd)?; } 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, 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 { 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 { 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 { 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. A pinned viewport caches the /// result; the first lookup counts backwards from the active end. pub fn viewport_row_offset(&self) -> usize { match self.viewport { Viewport::Active => self.active_offset(), Viewport::Top => 0, Viewport::Pin => { let cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); if cached != usize::MAX { return cached; } let Some(pin) = self.viewport_pin.and_then(|h| self.tracked_pin(h)) else { return 0; }; let mut offset = self.total_rows; let mut node = self.last; while let Some(id) = node { let n = self.node(id).expect("live viewport node"); offset -= usize::from(n.data.size.rows); if id == pin.node { offset += usize::from(pin.y); self.viewport_pin_row_offset .store(offset, Ordering::Relaxed); return offset; } node = n.prev; } 0 } } } fn invalidate_viewport_offset(&self) { self.viewport_pin_row_offset .store(usize::MAX, Ordering::Relaxed); } 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. Active and viewport /// coordinates start at their anchors, independent of history length. pub fn pin(&self, pt: Point) -> Option { let c = pt.coord(); if c.x >= self.cols { return None; } let mut p = self.get_top_left(pt.tag())?.down(self, c.y as usize)?; p.x = c.x; Some(p) } /// Convert a pin to a coordinate by traversing only its reference frame. pub fn point_from_pin(&self, tag: Tag, p: Pin) -> Option { if !self.pin_is_valid(p) { return None; } let top = self.get_top_left(tag)?; let mut node = top.node; let mut y = 0; let mut start = top.y; loop { if node == p.node { y += u32::from(p.y.checked_sub(start)?); return Some(Point::new(tag, Coordinate { x: p.x, y })); } let n = self.node(node)?; y += u32::from(n.data.size.rows - start); node = n.next?; start = 0; } } /// Top-left coordinate in a reference frame. The active area is always /// at the tail, so locating it never walks through scrollback. pub fn get_top_left(&self, tag: Tag) -> Option { match tag { Tag::Screen | Tag::History => Some(Pin { node: self.first?, x: 0, y: 0, garbage: false, }), Tag::Active => { let node = self.last?; Pin { node, x: 0, y: self.node(node)?.data.size.rows - 1, garbage: false, } .up(self, usize::from(self.rows) - 1) } Tag::Viewport => match self.viewport { Viewport::Active => self.get_top_left(Tag::Active), Viewport::Top => self.get_top_left(Tag::Screen), Viewport::Pin => self.viewport_pin.and_then(|h| self.tracked_pin(h)), }, } } /// Bottom-right coordinate in a reference frame, if the region exists. pub fn get_bottom_right(&self, tag: Tag) -> Option { let mut p = match tag { Tag::Screen | Tag::Active => { let node = self.last?; Pin { node, x: 0, y: self.node(node)?.data.size.rows - 1, garbage: false, } } Tag::Viewport => self .get_top_left(tag)? .down(self, usize::from(self.rows) - 1)?, Tag::History => self.get_top_left(Tag::Active)?.up(self, 1)?, }; p.x = self.cols - 1; Some(p) } fn pin_is_active(&self, p: Pin) -> bool { self.point_from_pin(Tag::Active, p).is_some() } /// 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 { 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 { self.pins .get(h.index) .filter(|s| s.generation == h.generation)? .pin } /// Update a tracked location. The node must be live; row bounds may be /// temporarily exceeded while a caller repairs cursor state after splitting. pub fn set_tracked_pin(&mut self, h: TrackedPin, p: Pin) -> Result<(), Error> { if self.node(p.node).is_none() { return Err(Error::InvalidId); } 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); if self.viewport_pin == Some(h) { self.invalidate_viewport_offset(); } Ok(()) } /// Mutable access to a tracked pin's location for hot paths that only /// change `x` within the same row (cursor movement while printing). /// Unlike [`PageList::set_tracked_pin`] this performs no node validation /// and does not invalidate the cached viewport offset, so it must not be /// used to move the pin to another row or node. #[inline] pub fn tracked_pin_x_mut(&mut self, h: TrackedPin) -> Option<&mut Pin> { self.pins .get_mut(h.index) .filter(|s| s.generation == h.generation)? .pin .as_mut() } /// 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 + '_ { 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, 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 cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); let viewport = (self.viewport == Viewport::Pin && cached != usize::MAX).then_some(cached); let n = self.detach(first).ok_or(Error::InvalidId)?; self.total_rows -= removed; if Page::layout(n.data.capacity).total_size <= std_size() { 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 let Some(viewport) = viewport { if viewport < removed { self.viewport = Viewport::Top; } else { self.viewport_pin_row_offset .store(viewport - removed, Ordering::Relaxed); } } 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; } match behavior { Scroll::Active => self.viewport = Viewport::Active, Scroll::Top => self.viewport = Viewport::Top, Scroll::Pin(p) => { if self.pin_is_active(p) { self.viewport = Viewport::Active; } else if Some(p.node) == self.first && p.y == 0 { self.viewport = Viewport::Top; } else if let Some(h) = self.viewport_pin { self.pins[h.index].pin = Some(p); self.viewport = Viewport::Pin; self.invalidate_viewport_offset(); } } Scroll::Row(n) => { if n == 0 { self.viewport = Viewport::Top; return; } if n >= self.active_offset() { self.viewport = Viewport::Active; return; } let cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); if self.viewport == Viewport::Pin && cached != usize::MAX { self.scroll(Scroll::DeltaRow(n as isize - cached as isize)); return; } let p = if n < self.total_rows / 2 { self.pin_at(n, 0) } else { self.get_bottom_right(Tag::Screen) .and_then(|p| p.up(self, self.total_rows - n - 1)) .map(|p| Pin { x: 0, ..p }) }; if let (Some(p), Some(h)) = (p, self.viewport_pin) { self.pins[h.index].pin = Some(p); self.viewport = Viewport::Pin; self.viewport_pin_row_offset.store(n, Ordering::Relaxed); } } Scroll::DeltaRow(n) => { if n == 0 || (self.viewport == Viewport::Active && n > 0) || (self.viewport == Viewport::Top && n < 0) { return; } let Some(top) = self.get_top_left(Tag::Viewport) else { return; }; let moved = if n < 0 { top.up(self, n.unsigned_abs()) } else { top.down(self, n as usize) }; let was_pin = self.viewport == Viewport::Pin; let cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); let Some(p) = moved else { self.viewport = if n < 0 { if self.active_offset() == 0 { Viewport::Active } else { Viewport::Top } } else { Viewport::Active }; return; }; // A pinned upward delta stays pinned even at row zero. if was_pin && (n < 0 || !self.pin_is_active(p)) { let h = self.viewport_pin.expect("viewport handle"); self.pins[h.index].pin = Some(p); if cached != usize::MAX { self.viewport_pin_row_offset .store(cached.saturating_add_signed(n), Ordering::Relaxed); } } else { self.scroll(Scroll::Pin(p)); } } Scroll::DeltaPrompt(delta) => { if delta == 0 { return; } let Some(top) = self.get_top_left(Tag::Viewport) else { return; }; let start = if delta < 0 { top.up(self, 1) } else { let mut next = top.down(self, 1); if top .row_and_cell(self) .is_some_and(|(r, _)| r.semantic_prompt() != SemanticPrompt::None) { while let Some(p) = next { if p.row_and_cell(self).is_none_or(|(r, _)| { r.semantic_prompt() != SemanticPrompt::PromptContinuation }) { break; } let following = p.down(self, 1); if following.is_none() { break; } next = following; } } next }; let Some(start) = start else { return; }; let direction = if delta < 0 { Direction::LeftUp } else { Direction::RightDown }; let mut it = PromptIterator { list: self, current: Some(start), limit: None, direction, }; let mut found = None; for _ in 0..delta.unsigned_abs() { let Some(p) = it.next() else { break; }; found = Some(p); } if let Some(p) = found { if self.pin_is_active(p) { self.viewport = Viewport::Active; } else if let Some(h) = self.viewport_pin { self.pins[h.index].pin = Some(p); self.viewport = Viewport::Pin; self.invalidate_viewport_offset(); } } } } } fn fixup_viewport(&mut self) { let pin = match self.viewport { Viewport::Active => return, Viewport::Top => self.get_top_left(Tag::Screen), Viewport::Pin => self.viewport_pin.and_then(|h| self.tracked_pin(h)), }; if pin.is_some_and(|p| self.pin_is_active(p)) { self.viewport = Viewport::Active; } } /// Read a cell without copying its managed auxiliary values. pub fn get_cell(&self, pt: Point) -> Option { 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) } /// The handle of the internal tracked viewport pin, if any (used by the /// inspector to label it). pub fn viewport_pin_handle(&self) -> Option { self.viewport_pin } /// 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 id = self.clone_node_rows(p.node, p.y, page.size.rows, page.capacity)?; 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(()) } /// Clone a range directly into a pooled destination, without allocating /// another temporary Page with the same backing storage. fn clone_node_rows( &mut self, source: NodeId, start: u16, end: u16, cap: Capacity, ) -> Result { let src = &self.node(source).ok_or(Error::InvalidId)?.data; let cols = src.size.cols; let dirty = src.dirty; let id = self.create_node(cap)?; let page = &mut self.node_mut(id).ok_or(Error::InvalidId)?.data; page.size.cols = cols; page.size.rows = end - start; page.dirty = dirty; for y in start..end { if let Err(error) = self.copy_row(source, y, id, y - start) { self.destroy_unlinked(id); return Err(error); } } Ok(id) } fn destroy_unlinked(&mut self, id: NodeId) { let node = self.nodes[id.index].take().expect("unlinked live node"); self.page_size -= allocated_size(node.data.capacity); self.free_nodes.push(id.index); self.recycle(node); } fn replace_page(&mut self, old: NodeId, id: NodeId) -> Result { let prev = self.node(old).ok_or(Error::InvalidId)?.prev; 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, ) -> Result { 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 new = self.clone_node_rows(id, 0, old.size.rows, cap)?; self.replace_page(id, new) } /// Alias for capacity adjustment used by higher-level screen code. pub fn adjust_capacity( &mut self, id: NodeId, adjustment: Option, ) -> Result { self.increase_capacity(id, adjustment) } /// Compact oversized pages when a smaller logical allocation is possible. pub fn compact(&mut self, id: NodeId) -> Result, 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 new = match self.clone_node_rows(id, 0, old.size.rows, cap) { Ok(new) => new, Err(Error::OutOfMemory) => return Err(Error::OutOfMemory), Err(_) => return Ok(None), }; self.replace_page(id, new).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(()); } self.invalidate_viewport_offset(); let mut node = self.first; let mut offset = 0; while let Some(id) = node { let current = self.node(id).ok_or(Error::InvalidId)?; node = current.next; let rows = usize::from(current.data.size.rows); let a = start.saturating_sub(offset).min(rows); let b = end.saturating_sub(offset).min(rows); offset += rows; if a == b { continue; } if a == 0 && b == rows && (current.prev.is_some() || current.next.is_some()) { let dest = current.prev.or(current.next).ok_or(Error::InvalidId)?; for slot in &mut self.pins { if let Some(p) = &mut slot.pin && p.node == id { p.node = dest; p.x = 0; p.y = 0; } } let old = self.detach(id).ok_or(Error::InvalidId)?; self.recycle(old); } else if a == 0 && b == rows { let page = &mut self.node_mut(id).ok_or(Error::InvalidId)?.data; let cols = page.size.cols; page.reinit()?; page.size.cols = cols; page.size.rows = 0; } else { let page = &mut self.node_mut(id).ok_or(Error::InvalidId)?.data; for y in a..b { page.clear_cells(y, 0, usize::from(page.size.cols), false)?; } page.rows[a..rows].rotate_left(b - a); page.size.rows -= (b - a) as u16; for row in &mut page.rows[a..usize::from(page.size.rows)] { row.set_dirty(true); } for slot in &mut self.pins { if let Some(p) = &mut slot.pin && p.node == id { if usize::from(p.y) >= b { p.y -= (b - a) as u16; } else if usize::from(p.y) >= a { p.y = 0; p.x = 0; } } } } if offset >= end { break; } } self.total_rows -= end - start; Ok(()) } /// Remove an inclusive row range, regrowing erased active rows. pub fn erase_rows(&mut self, start: Point, end: Option) -> Result<(), Error> { // An empty history produces no chunks in the reference iterator. if start.tag() == Tag::History && self.active_offset() == 0 && end.is_none() { return Ok(()); } 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 }; let cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); self.erase_range(a, b)?; let regrow = if start.tag() == Tag::Active { b - a } else { usize::from(self.rows).saturating_sub(self.total_rows) }; for _ in 0..regrow { self.grow()?; } self.fixup_viewport(); if a == 0 && self.viewport == Viewport::Pin && cached != usize::MAX { if cached < b - a { self.viewport = Viewport::Top; } else { self.viewport_pin_row_offset .store(cached - (b - a), Ordering::Relaxed); } } self.fixup_viewport(); Ok(()) } /// Remove one row, shifting following rows without changing page dimensions. pub fn erase_row(&mut self, pt: Point) -> Result<(), Error> { self.erase_row_bounded_impl(pt, usize::MAX, true)?; self.invalidate_viewport_offset(); self.fixup_viewport(); 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> { self.erase_row_bounded_impl(pt, limit, false) } fn erase_row_bounded_impl( &mut self, pt: Point, limit: usize, keep_erased_pin: bool, ) -> Result<(), Error> { let first = self.pin(pt).ok_or(Error::InvalidCoordinate)?; let mut node = first.node; let mut start = first.y; let mut remaining = limit; loop { let current = self.node(node).ok_or(Error::InvalidId)?; let rows = current.data.size.rows; let next = current.next; let previous = current.prev; let previous_rows = previous .and_then(|p| self.node(p)) .map(|n| n.data.size.rows); let count = (remaining.saturating_add(1)).min(usize::from(rows - start)); let end = start + count as u16; let stop = usize::from(rows - start) > remaining; let page = &mut self.node_mut(node).ok_or(Error::InvalidId)?.data; if stop { page.clear_cells(usize::from(start), 0, usize::from(page.size.cols), false)?; } page.rows[usize::from(start)..usize::from(end)].rotate_left(1); page.dirty = true; if self.viewport == Viewport::Pin && let Some(p) = self.viewport_pin.and_then(|h| self.tracked_pin(h)) && p.node == node && p.y >= start && p.y < end && (node != first.node || p.y > 0) { let cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); if cached != usize::MAX { self.viewport_pin_row_offset .store(cached - 1, Ordering::Relaxed); } } for slot in &mut self.pins { if let Some(p) = &mut slot.pin && p.node == node && p.y >= start && p.y < end { if keep_erased_pin && node == first.node && p.y == first.y { continue; } if p.y > 0 { p.y -= 1; } else if node == first.node { p.x = 0; } else { p.node = previous.ok_or(Error::InvalidId)?; p.y = previous_rows.ok_or(Error::InvalidId)? - 1; } } } if stop { return Ok(()); } if let Some(next) = next { self.copy_row(next, 0, node, rows - 1)?; remaining -= usize::from(rows - start); node = next; start = 0; } else { let page = &mut self.node_mut(node).ok_or(Error::InvalidId)?.data; page.clear_cells(usize::from(rows - 1), 0, usize::from(page.size.cols), false)?; return 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)?; if src.node == dst.node { self.node_mut(src.node) .ok_or(Error::InvalidId)? .data .clone_cells_within( usize::from(src.y), usize::from(dst.y), usize::from(left), usize::from(left), usize::from(right - left + 1), )?; } else { let (source, dest) = self.pages_for_copy(src.node, dst.node)?; let base = source.rows[usize::from(src.y)].cells(); for x in left..=right { dest.clone_cell_from( source, base + usize::from(x), usize::from(x), usize::from(dst.y), )?; } } } 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); } let cap = initial_capacity(self.cols); let pages = usize::from(self.rows).div_ceil(usize::from(cap.rows)); self.page_pool.retain(|page| page.capacity == cap); self.page_pool.truncate(pages); self.nodes.clear(); self.free_nodes.clear(); self.nodes.shrink_to(pages); self.free_nodes.shrink_to(pages); 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 { let mut p = self; let mut remaining = n; loop { if remaining <= usize::from(p.y) { p.y -= remaining as u16; return Some(Overflow::Offset(p)); } remaining -= usize::from(p.y); let Some(prev) = list.node(p.node)?.prev else { p.y = 0; return Some(Overflow::Overflow { end: p, remaining }); }; remaining -= 1; p.node = prev; p.y = list.node(prev)?.data.size.rows - 1; } } /// Move downward, returning the edge and unconsumed distance on overflow. pub fn down_overflow(self, list: &PageList, n: usize) -> Option { let mut p = self; let mut remaining = n; loop { let node = list.node(p.node)?; let available = usize::from(node.data.size.rows - p.y - 1); if remaining <= available { p.y += remaining as u16; return Some(Overflow::Offset(p)); } remaining -= available; let Some(next) = node.next else { p.y = node.data.size.rows - 1; return Some(Overflow::Overflow { end: p, remaining }); }; remaining -= 1; p.node = next; p.y = 0; } } /// 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { if self.node == other.node { return (self.y, self.x) < (other.y, other.x); } let mut node = list.node(self.node).and_then(|n| n.next); while let Some(id) = node { if id == other.node { return true; } node = list.node(id).and_then(|n| n.next); } 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 { 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) } } impl Pin { /// Iterate cells toward an inclusive limit row. The limit column is ignored. pub fn cell_iterator( self, list: &PageList, direction: Direction, limit: Option, ) -> CellIterator<'_> { let (front, back) = match direction { Direction::RightDown => ( Some(self), limit .or_else(|| list.get_bottom_right(Tag::Screen)) .map(|p| Pin { x: list.cols - 1, ..p }), ), Direction::LeftUp => ( limit .or_else(|| list.get_top_left(Tag::Screen)) .map(|p| Pin { x: 0, ..p }), Some(self), ), }; CellIterator { list, front, back, direction, } } /// Iterate rows directly from a pin, with an inclusive limit row. pub fn row_iterator( self, list: &PageList, direction: Direction, limit: Option, ) -> RowIterator<'_> { let cells = self.cell_iterator(list, direction, limit); RowIterator { list, front: cells.front.map(|p| Pin { x: 0, ..p }), back: cells.back.map(|p| Pin { x: 0, ..p }), direction, } } /// Iterate maximal page chunks directly from a pin. pub fn page_iterator( self, list: &PageList, direction: Direction, limit: Option, ) -> PageIterator<'_> { PageIterator { rows: self.row_iterator(list, direction, limit), } } /// Iterate prompt starts directly from a pin, with an inclusive limit row. pub fn prompt_iterator( self, list: &PageList, direction: Direction, limit: Option, ) -> PromptIterator<'_> { PromptIterator { list, current: Some(self), limit, direction, } } } /// A double-ended cell iterator yielding stable untracked locations. pub struct CellIterator<'a> { list: &'a PageList, front: Option, back: Option, direction: Direction, } impl CellIterator<'_> { fn take_front(&mut self) -> Option { 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 { 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 { if self.direction == Direction::RightDown { self.take_front() } else { self.take_back() } } } impl DoubleEndedIterator for CellIterator<'_> { fn next_back(&mut self) -> Option { 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, back: Option, direction: Direction, } impl RowIterator<'_> { fn take_front(&mut self) -> Option { 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 { 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 { if self.direction == Direction::RightDown { self.take_front() } else { self.take_back() } } } impl DoubleEndedIterator for RowIterator<'_> { fn next_back(&mut self) -> Option { 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 { 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 { 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 { if self.rows.direction == Direction::RightDown { self.front() } else { self.back() } } } impl DoubleEndedIterator for PageIterator<'_> { fn next_back(&mut self) -> Option { if self.rows.direction == Direction::RightDown { self.back() } else { self.front() } } } impl PageList { fn iter_bounds(&self, start: Point, end: Option) -> (Option, Option) { 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, ) -> 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, ) -> 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, ) -> 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, ) -> 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, limit: Option, direction: Direction, } impl Iterator for PromptIterator<'_> { type Item = Pin; fn next(&mut self) -> Option { 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; } } } /// Capacity inherited by a rewritten page. Oversized source pages cap their /// inherited row count on reflow to avoid propagating massive empty grids. fn resized_capacity(page: &Page, cols: u16, reflow: bool) -> Capacity { page.capacity .adjust(Adjustment { cols: Some(cols) }) .unwrap_or(Capacity { cols, rows: page.size.rows.min(if reflow { page::STD_CAPACITY.rows } else { page.capacity.rows }), ..page.capacity }) } /// Streaming source-to-destination cursor used exclusively during reflow. /// Unlike a Screen cursor it owns no rendering or terminal state. struct ReflowCursor { node: NodeId, x: u16, y: u16, pending_wrap: bool, new_rows: usize, total_rows: usize, } #[derive(Copy, Clone)] enum WriteResult { Success, Repeat, SkipNext, } impl ReflowCursor { fn new(node: NodeId) -> Self { Self { node, x: 0, y: 0, pending_wrap: false, new_rows: 0, total_rows: 1, } } fn page<'a>(&self, list: &'a PageList) -> &'a Page { &list.node(self.node).expect("reflow destination").data } fn page_mut<'a>(&self, list: &'a mut PageList) -> &'a mut Page { &mut list.node_mut(self.node).expect("reflow destination").data } fn forward(&mut self, list: &PageList) { if self.x == self.page(list).size.cols - 1 { self.pending_wrap = true; } else { self.x += 1; } } fn scroll(&mut self, list: &mut PageList, cap: Capacity) -> Result<(), Error> { if self.y == self.page(list).capacity.rows - 1 { let node = list.create_node(cap)?; list.node_mut(node).ok_or(Error::InvalidId)?.data.size.rows = 1; list.insert_after(Some(self.node), node); self.node = node; self.y = 0; } else { self.page_mut(list).size.rows += 1; self.y += 1; } self.x = 0; self.pending_wrap = false; self.total_rows += 1; Ok(()) } fn increase_capacity( &mut self, list: &mut PageList, adjustment: Option, ) -> Result<(), Error> { self.node = list.increase_capacity(self.node, adjustment)?; self.pending_wrap = false; // Zig reinitializes the cursor after replacing the page. self.new_rows = 0; Ok(()) } fn remap_cell(&self, list: &mut PageList, source: NodeId, y: u16, x: u16) { for slot in &mut list.pins { if let Some(p) = &mut slot.pin && p.node == source && p.y == y && p.x == x { p.node = self.node; p.x = self.x; p.y = self.y; } } } fn reflow_row( &mut self, list: &mut PageList, source: NodeId, src: &Page, y: u16, ) -> Result<(), Error> { let row = src.rows[usize::from(y)]; let cells = src .get_cells(usize::from(y)) .ok_or(Error::InvalidCoordinate)?; let mut len = src.size.cols; if !row.wrap() { while len > 0 && cells[usize::from(len - 1)].is_empty() { len -= 1; } if len == 0 && row.semantic_prompt() != SemanticPrompt::None { len = 1; } } let remaining = self.page(list).size.cols - 1 - self.x; for slot in &mut list.pins { if let Some(p) = &mut slot.pin && p.node == source && p.y == y { if p.x >= len { p.x = p.x.min(remaining); } len = len.max(p.x + 1); } } if len == 0 { if !row.wrap_continuation() { self.new_rows += 1; } return Ok(()); } let cap = resized_capacity(src, self.page(list).size.cols, true); while self.new_rows > 0 { self.scroll(list, cap)?; self.new_rows -= 1; } self.page_mut(list).rows[usize::from(self.y)].set_semantic_prompt(row.semantic_prompt()); let mut x = 0; while x < len { if self.pending_wrap { self.page_mut(list).rows[usize::from(self.y)].set_wrap(true); self.scroll(list, cap)?; let dst = &mut self.page_mut(list).rows[usize::from(self.y)]; dst.set_semantic_prompt(row.semantic_prompt()); dst.set_wrap_continuation(true); } self.remap_cell(list, source, y, x); let index = row.cells() + usize::from(x); match self.write_cell(list, src, index) { Ok(WriteResult::Success) => x += 1, Ok(WriteResult::Repeat) => {} Ok(WriteResult::SkipNext) => { self.remap_cell(list, source, y, x + 1); x += 2; } Err(Error::OutOfSpace) if self.y == 0 => { x += 1; self.forward(list); } Err(Error::OutOfSpace) => self.move_last_row(list, cap)?, Err(error) => return Err(error), } } if !row.wrap() { self.new_rows += 1; } Ok(()) } /// Install unmanaged content first. A capacity failure leaves a valid cell /// whose managed parts can be copied to a new page before retrying. fn write_cell( &mut self, list: &mut PageList, src: &Page, index: usize, ) -> Result { let cell = src.cells[index]; let cols = self.page(list).size.cols; let x = usize::from(self.x); let y = usize::from(self.y); let dst = self.page(list).cell_index(x, y)?; if matches!( cell.content_tag(), ContentTag::BgColorPalette | ContentTag::BgColorRgb ) { self.page_mut(list).cells[dst] = cell; self.forward(list); return Ok(WriteResult::Success); } match cell.wide() { Wide::Wide if cols == 1 => { let c = &mut self.page_mut(list).cells[dst]; c.set_codepoint(0); c.set_wide(Wide::Narrow); self.forward(list); return Ok(WriteResult::SkipNext); } Wide::Wide if self.x == cols - 1 => { let mut spacer = Cell::default(); spacer.set_wide(Wide::SpacerHead); self.page_mut(list).cells[dst] = spacer; self.forward(list); return Ok(WriteResult::Repeat); } Wide::SpacerHead => return Ok(WriteResult::Success), Wide::SpacerTail if cols == 1 => return Ok(WriteResult::Success), _ => {} } // Only a retry can overwrite owned data. Ordinary reflow writes into // fresh cells, avoiding both cleanup and whole-row metadata scans. let previous = self.page(list).cells[dst]; if previous.has_grapheme() || previous.hyperlink() || previous.has_styling() { let dirty = self.page(list).rows[y].dirty(); self.page_mut(list).clear_cells(y, x, x + 1, false)?; self.page_mut(list).rows[y].set_dirty(dirty); } let target = &mut self.page_mut(list).cells[dst]; *target = cell; target.set_content_tag(ContentTag::Codepoint); target.set_hyperlink(false); target.set_style_id(0); if cell.codepoint() == 0x10eeee { self.page_mut(list).rows[y].set_kitty_virtual_placeholder(true); } if cell.has_grapheme() { let cps = src.lookup_grapheme(index).ok_or(Error::InvalidId)?; if self.page(list).grapheme_count() >= self.page(list).grapheme_capacity() { self.increase_capacity(list, Some(IncreaseCapacity::GraphemeBytes))?; } while !self.page_mut(list).can_allocate_grapheme(cps.len() * 4) { self.increase_capacity(list, Some(IncreaseCapacity::GraphemeBytes))?; } loop { let dirty = self.page(list).rows[y].dirty(); let result = self.page_mut(list).set_graphemes(x, y, cps); self.page_mut(list).rows[y].set_dirty(dirty); match result { Ok(()) => break, Err(Error::GraphemeMapOutOfMemory | Error::GraphemeAllocOutOfMemory) => { self.increase_capacity(list, Some(IncreaseCapacity::GraphemeBytes))?; } Err(error) => return Err(error), } } } if cell.hyperlink() { loop { match self.page_mut(list).clone_hyperlink_from(src, index, x, y) { Ok(()) => break, Err(Error::HyperlinkMapOutOfMemory | Error::SetOutOfMemory) => { self.increase_capacity(list, Some(IncreaseCapacity::HyperlinkBytes))?; } Err(Error::StringsOutOfMemory) => { self.increase_capacity(list, Some(IncreaseCapacity::StringBytes))?; } Err(Error::SetNeedsRehash) => self.increase_capacity(list, None)?, Err(error) => return Err(error), } } } if cell.has_styling() { let style = *src.styles.get(cell.style_id()).ok_or(Error::InvalidId)?; let id = loop { match self .page_mut(list) .styles .add_with_id(style, cell.style_id()) { Ok(id) => break id.unwrap_or(cell.style_id()), Err(crate::ref_counted_set::AddError::OutOfMemory) => { self.increase_capacity(list, Some(IncreaseCapacity::Styles))?; } Err(crate::ref_counted_set::AddError::NeedsRehash) => { self.increase_capacity(list, None)?; } Err(error) => return Err(error.into()), } }; let page = self.page_mut(list); let dst = page.cell_index(x, y)?; page.cells[dst].set_style_id(id); page.rows[y].set_styled(true); } self.forward(list); Ok(WriteResult::Success) } /// Split off the current final row and resume at its previous column. fn move_last_row(&mut self, list: &mut PageList, cap: Capacity) -> Result<(), Error> { let old_node = self.node; let old_y = self.y; let node = list.create_node(cap)?; list.node_mut(node).ok_or(Error::InvalidId)?.data.size.rows = 1; list.insert_after(Some(old_node), node); list.copy_row(old_node, old_y, node, 0)?; list.remap_rows(old_node, old_y, old_y + 1, node, 0); let old = &mut list.node_mut(old_node).ok_or(Error::InvalidId)?.data; old.clear_cells(usize::from(old_y), 0, usize::from(old.size.cols), false)?; old.size.rows -= 1; if old.size.rows == 0 { let old = list.detach(old_node).ok_or(Error::InvalidId)?; list.recycle(old); } self.node = node; self.y = 0; self.pending_wrap = false; Ok(()) } } impl PageList { /// Trim trailing rows without text, stopping at any tracked location. fn trim_blank_rows(&mut self, limit: usize) -> Result<(), Error> { for _ in 0..limit { let Some(node) = self.last else { break; }; let page = &self.node(node).ok_or(Error::InvalidId)?.data; let y = page.size.rows - 1; if Cell::has_text_any( page.get_cells(usize::from(y)) .ok_or(Error::InvalidCoordinate)?, ) || self.tracked_pins().any(|(_, p)| p.node == node && p.y == y) { break; } let page = &mut self.node_mut(node).ok_or(Error::InvalidId)?.data; page.size.rows -= 1; self.total_rows -= 1; if y == 0 { let old = self.detach(node).ok_or(Error::InvalidId)?; self.recycle(old); } } Ok(()) } /// Resize the active area and optionally reflow wrapped logical lines. /// Column growth unwraps before resizing rows; column shrink resizes rows /// first so wrapping has the correct active height available. 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); } self.invalidate_viewport_offset(); self.min_max_size = min_max_size(cols, rows); if !opts.reflow || cols == self.cols { self.resize_without_reflow(opts)?; } else if cols > self.cols { self.resize_cols(cols, opts.cursor)?; self.resize_without_reflow(opts)?; } else { self.resize_without_reflow(Resize { cols: Some(self.cols), ..opts })?; self.resize_cols(cols, opts.cursor)?; } if self.viewport == Viewport::Pin { self.fixup_viewport(); } Ok(()) } fn resize_without_reflow(&mut self, opts: Resize) -> Result<(), Error> { if let Some(cols) = opts.cols { if cols < self.cols { let mut node = self.first; while let Some(id) = node { let n = self.node_mut(id).ok_or(Error::InvalidId)?; node = n.next; for y in 0..usize::from(n.data.size.rows) { n.data.clear_cells( y, usize::from(cols), usize::from(n.data.size.cols), false, )?; } n.data.size.cols = cols; } for slot in &mut self.pins { if let Some(p) = &mut slot.pin { p.x = p.x.min(cols - 1); } } self.cols = cols; } else if cols > self.cols { let mut node = self.first; self.cols = cols; while let Some(id) = node { node = self.node(id).ok_or(Error::InvalidId)?.next; self.resize_without_reflow_grow_cols(id, cols)?; } } } if let Some(rows) = opts.rows { if rows < self.rows { self.trim_blank_rows(usize::from(self.rows - rows))?; self.rows = rows; } else if rows > self.rows { let old_rows = self.rows; self.rows = rows; if opts.cursor.is_some_and(|c| c.y < old_rows - 1) { for _ in old_rows..rows { self.grow()?; } } else { while self.total_rows < usize::from(rows) { self.grow()?; } if self.viewport == Viewport::Pin { self.fixup_viewport(); } } } } Ok(()) } /// Clone a complete row between two page locations, ignoring columns. pub fn clone_row_between(&mut self, src: Pin, dst: Pin) -> Result<(), Error> { if src.node == dst.node { let cols = usize::from(self.node(src.node).ok_or(Error::InvalidId)?.data.size.cols); return self.clone_partial_row_between(src, dst, 0, cols); } self.copy_row(src.node, src.y, dst.node, dst.y) } /// Clone columns `[left, right)` between rows without a temporary row copy. pub fn clone_partial_row_between( &mut self, src: Pin, dst: Pin, left: usize, right: usize, ) -> Result<(), Error> { let len = right.checked_sub(left).ok_or(Error::InvalidCoordinate)?; if src.node == dst.node { let page = &mut self.node_mut(src.node).ok_or(Error::InvalidId)?.data; return page.clone_partial_row_within( usize::from(src.y), usize::from(dst.y), left, len, ); } let (source, dest) = self.pages_for_copy(src.node, dst.node)?; dest.clone_partial_row_from( source, usize::from(src.y), usize::from(dst.y), left, left, len, ) } /// Borrow distinct source and destination pages without snapshots or unsafe /// pointers. Their slab slots remain stable for the duration of the copy. fn copy_row(&mut self, src: NodeId, src_y: u16, dst: NodeId, dst_y: u16) -> Result<(), Error> { let (source, dest) = self.pages_for_copy(src, dst)?; dest.clone_row_from(source, usize::from(src_y), usize::from(dst_y)) } fn pages_for_copy(&mut self, src: NodeId, dst: NodeId) -> Result<(&Page, &mut Page), Error> { if self.node(src).is_none() || self.node(dst).is_none() || src.index == dst.index { return Err(Error::InvalidId); } let (source, dest) = if src.index < dst.index { let (left, right) = self.nodes.split_at_mut(dst.index); (left[src.index].as_ref(), right[0].as_mut()) } else { let (left, right) = self.nodes.split_at_mut(src.index); (right[0].as_ref(), left[dst.index].as_mut()) }; Ok(( &source.ok_or(Error::InvalidId)?.data, &mut dest.ok_or(Error::InvalidId)?.data, )) } /// Widen one page, first backfilling the previously rewritten page to avoid /// permanently stranding row capacity between page boundaries. fn resize_without_reflow_grow_cols(&mut self, source: NodeId, cols: u16) -> Result<(), Error> { let src = &self.node(source).ok_or(Error::InvalidId)?.data; if src.capacity.cols >= cols && (0..usize::from(src.size.rows)).all(|y| { src.get_cells(y).expect("source row")[usize::from(src.size.cols - 1)].wide() != Wide::SpacerHead }) { self.node_mut(source) .ok_or(Error::InvalidId)? .data .size .cols = cols; return Ok(()); } let cap = resized_capacity(src, cols, false); let rows = src.size.rows; let previous = self.node(source).ok_or(Error::InvalidId)?.prev; let mut copied = 0; if let Some(prev) = previous { let page = &self.node(prev).ok_or(Error::InvalidId)?.data; let start = page.size.rows; let len = (page.capacity.rows - start).min(rows); for y in 0..len { self.node_mut(prev).ok_or(Error::InvalidId)?.data.size.rows += 1; if self.copy_row(source, y, prev, start + y).is_err() { let page = &mut self.node_mut(prev).ok_or(Error::InvalidId)?.data; page.clear_cells(usize::from(start + y), 0, usize::from(cols), false)?; page.size.rows -= 1; break; } copied += 1; } self.remap_rows(source, 0, copied, prev, start); } while copied < rows { let id = self.create_node(cap)?; let start = copied; let len = cap.rows.min(rows - copied); self.insert_after(self.node(source).ok_or(Error::InvalidId)?.prev, id); for y in 0..len { self.node_mut(id).ok_or(Error::InvalidId)?.data.size.rows += 1; self.copy_row(source, copied, id, y)?; copied += 1; } self.remap_rows(source, start, copied, id, 0); } let old = self.detach(source).ok_or(Error::InvalidId)?; self.recycle(old); Ok(()) } fn remap_rows(&mut self, source: NodeId, start: u16, end: u16, dest: NodeId, dest_y: u16) { for slot in &mut self.pins { if let Some(p) = &mut slot.pin && p.node == source && p.y >= start && p.y < end { p.node = dest; p.y = p.y - start + dest_y; } } } fn wrapped_rows_above(&self, pin: Pin) -> usize { let Some(top) = self.get_top_left(Tag::Active) else { return 0; }; let mut current = Some(pin); let mut count = 0; while let Some(p) = current { if p.row_and_cell(self) .is_some_and(|(r, _)| r.wrap_continuation()) { count += 1; } if p.node == top.node && p.y == top.y { break; } current = p.up(self, 1); } count } /// Rewrite incrementally, releasing each source page as soon as it has been /// consumed. Pending blank rows are only materialized before later content. fn resize_cols(&mut self, cols: u16, cursor: Option) -> Result<(), Error> { self.cols = cols; let preserved = if let Some(c) = cursor { if let Some(p) = self.pin(Point::Active(Coordinate { x: c.x, y: u32::from(c.y), })) { let wrapped = self.wrapped_rows_above(p); Some(( self.track_pin(p)?, usize::from(self.rows - c.y - 1), wrapped, )) } else { None } } else { None }; let result = self.reflow_pages(cols); if let Err(error) = result { if let Some((handle, _, _)) = preserved { self.untrack_pin(handle); } return Err(error); } while self.total_rows < usize::from(self.rows) { self.grow()?; } if let Some((handle, remaining, old_wrapped)) = preserved { let p = self.tracked_pin(handle).ok_or(Error::InvalidId)?; if let Some(point) = self.point_from_pin(Tag::Active, p) { let wrapped = self.wrapped_rows_above(p); let current = usize::from(self.rows) - point.coord().y as usize - 1; let required = remaining .saturating_sub(wrapped.saturating_sub(old_wrapped)) .saturating_sub(current); for _ in 0..required { self.grow()?; } } self.untrack_pin(handle); } Ok(()) } fn reflow_pages(&mut self, cols: u16) -> Result<(), Error> { let mut old = self.first; let first = old.ok_or(Error::InvalidId)?; let cap = resized_capacity(&self.node(first).ok_or(Error::InvalidId)?.data, cols, false); let rewritten = self.create_node(cap)?; self.node_mut(rewritten) .ok_or(Error::InvalidId)? .data .size .rows = 1; self.first = Some(rewritten); self.last = Some(rewritten); let mut cursor = ReflowCursor::new(rewritten); while let Some(id) = old { // Taking a source node out of the slab permits borrowing its page // while allocating or replacing destination pages in the list. let source = self.nodes[id.index].take().ok_or(Error::InvalidId)?; old = source.next; let result = (0..source.data.size.rows) .try_for_each(|y| cursor.reflow_row(self, id, &source.data, y)); self.page_size -= allocated_size(source.data.capacity); self.free_nodes.push(id.index); self.recycle(source); if let Err(error) = result { while let Some(id) = old { let source = self.nodes[id.index].take().ok_or(Error::InvalidId)?; old = source.next; self.page_size -= allocated_size(source.data.capacity); self.free_nodes.push(id.index); self.recycle(source); } return Err(error); } } self.total_rows = cursor.total_rows; Ok(()) } /// Clone inclusive whole rows, preserving source page capacities and /// boundaries. Pins in copied rows receive independent tracked handles. pub fn clone_range( &self, start: Point, end: Option, ) -> Result<(Self, HashMap), Error> { let mut result = Self::init(self.cols, self.rows, Some(self.explicit_max_size))?; while let Some(id) = result.first { let node = result.detach(id).ok_or(Error::InvalidId)?; // Freshly initialised and never written: reuse without re-zeroing. result.clean_pool.push(node.data); } result.page_serial = 0; result.total_rows = 0; result.min_max_size = self.min_max_size; let mut remap = HashMap::new(); for chunk in self.page_iterator(Direction::RightDown, start, end) { let source = &self.node(chunk.node).ok_or(Error::InvalidId)?.data; let node = result.create_node(source.capacity)?; let dest = &mut result.node_mut(node).ok_or(Error::InvalidId)?.data; dest.size.cols = source.size.cols; dest.size.rows = chunk.end - chunk.start; dest.clone_from(source, usize::from(chunk.start), usize::from(chunk.end))?; dest.dirty = source.dirty; result.insert_after(result.last, node); result.total_rows += usize::from(chunk.end - chunk.start); for (handle, p) in self.tracked_pins() { if Some(handle) == self.viewport_pin || p.node != chunk.node || p.y < chunk.start || p.y >= chunk.end { continue; } remap.insert( handle, result.track_pin(Pin { node, y: p.y - chunk.start, ..p })?, ); } } let first = result.first.ok_or(Error::InvalidCoordinate)?; let viewport = result.viewport_pin.ok_or(Error::InvalidId)?; result.pins[viewport.index].pin = Some(Pin { node: first, x: 0, y: 0, garbage: false, }); while result.total_rows < usize::from(result.rows) { result.grow()?; } Ok((result, remap)) } /// Fallibly clone the whole screen with independently owned tracked handles. pub fn try_clone(&self) -> Result { 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()); } } let cached = self.viewport_pin_row_offset.load(Ordering::Relaxed); if self.viewport == Viewport::Pin && cached != usize::MAX { let pin = self .viewport_pin .and_then(|h| self.tracked_pin(h)) .ok_or("missing viewport pin")?; if self.screen_row(pin) != Some(cached) { return Err("stale viewport row offset".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 { if v == u16::MAX { return Err(Error::OutOfSpace); } Ok(v.saturating_mul(2)) } fn double32(v: u32) -> Result { if v == u32::MAX { return Err(Error::OutOfSpace); } Ok(v.saturating_mul(2)) } #[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 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 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 mut s = PageList::init(80, 24, None).unwrap(); assert_eq!(s.viewport, Viewport::Active); assert!(s.first.is_some()); assert_eq!(s.total_rows, usize::from(s.rows)); assert_eq!( s.tracked_pin(s.viewport_pin.unwrap()).unwrap().node, s.first.unwrap() ); assert_eq!( s.get_top_left(Tag::Active), Some(Pin { node: s.first.unwrap(), x: 0, y: 0, garbage: false }) ); assert_scrollbar(&s, 0); // Keep a mutable binding to mirror construction/destruction ownership. s.clear_dirty(); } #[test] fn page_list_init_error() { use crate::tripwire::ResetMode; // Test every failure point in `init` and ensure we exit early // without panicking or leaving the tripwire armed. for tag in [ InitPoint::InitPages, InitPoint::ViewportPin, InitPoint::ViewportPinTrack, ] { init_tw::error_always(tag, Error::OutOfMemory); assert_eq!( Err(Error::OutOfMemory), PageList::init(80, 24, None).map(drop) ); init_tw::end(ResetMode::Reset).unwrap(); } // init creates pages transitively, so check each failure there too. let big = page::STD_CAPACITY.max_cols().unwrap() + 1; for tag in [ InitPagesPoint::Node, InitPagesPoint::BufStd, InitPagesPoint::BufNonStd, ] { init_pages_tw::error_always(tag, Error::OutOfMemory); let cols = if tag == InitPagesPoint::BufStd { 80 } else { big }; assert_eq!( Err(Error::OutOfMemory), PageList::init(cols, 24, None).map(drop) ); init_pages_tw::end(ResetMode::Reset).unwrap(); } // Non-standard pages: fail on the second one. init_pages_tw::error_after(InitPagesPoint::BufNonStd, Error::OutOfMemory, 1); assert_eq!( Err(Error::OutOfMemory), PageList::init(big, page::STD_CAPACITY.rows + 1, None).map(drop) ); init_pages_tw::end(ResetMode::Reset).unwrap(); // And with nothing armed init still works. PageList::init(80, 24, None).unwrap(); } #[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.viewport, Viewport::Active); assert!(s.first.is_some()); assert_eq!(s.total_rows, usize::from(s.rows)); assert_scrollbar(&s, 0); } #[test] fn page_list_init_more_than_max_cols() { let s = PageList::init(page::STD_CAPACITY.max_cols().unwrap() + 1, 80, None).unwrap(); assert_eq!(s.viewport, Viewport::Active); assert_eq!(s.total_rows, usize::from(s.rows)); assert!(s.first.is_some()); assert!( Page::layout(s.node(s.first.unwrap()).unwrap().data.capacity).total_size > std_size() ); assert_scrollbar(&s, 0); } #[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(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 10); assert_eq!(s.total_rows, usize::from(s.rows) + 10); assert_viewport_point(&s, 10); for (tag, y) in [(Tag::Screen, 0), (Tag::Active, 10)] { assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(tag).unwrap()), Some(screen_point(0, y)) ); } assert_scrollbar(&s, 10); } #[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_rows(&mut s, 10); assert_viewport_point(&s, 10); s.scroll(Scroll::Top); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); grow_rows(&mut s, 10); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); s.scroll(Scroll::Active); assert_viewport_point(&s, 20); assert_scrollbar(&s, 20); } #[test] fn page_list_scroll_delta_row_back() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); assert_viewport_point(&s, 10); s.scroll(Scroll::DeltaRow(-1)); assert_viewport_point(&s, 9); assert_scrollbar(&s, 9); grow_rows(&mut s, 10); assert_viewport_point(&s, 9); assert_scrollbar(&s, 9); s.scroll(Scroll::DeltaRow(-1)); assert_scrollbar(&s, 8); } #[test] fn page_list_scroll_delta_row_back_overflow() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); assert_viewport_point(&s, 10); s.scroll(Scroll::DeltaRow(-100)); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); grow_rows(&mut s, 10); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); } #[test] fn page_list_scroll_delta_row_forward() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); assert_viewport_point(&s, 10); s.scroll(Scroll::Top); s.scroll(Scroll::DeltaRow(2)); assert_viewport_point(&s, 2); assert_scrollbar(&s, 2); grow_rows(&mut s, 10); assert_viewport_point(&s, 2); assert_scrollbar(&s, 2); } #[test] fn page_list_scroll_delta_row_forward_into_active() { let mut s = PageList::init(80, 24, None).unwrap(); s.scroll(Scroll::DeltaRow(2)); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); } #[test] fn page_list_scroll_to_pin() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); for y in [4, 5] { s.scroll(Scroll::Pin(s.pin(screen_point(2, y)).unwrap())); assert_scrollbar(&s, y); assert_viewport_point(&s, y); } } #[test] fn page_list_scroll_to_pin_in_active() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); s.scroll(Scroll::Pin(s.pin(screen_point(2, 30)).unwrap())); assert_viewport_point(&s, 10); assert_scrollbar(&s, 10); } #[test] fn page_list_scroll_to_pin_at_top() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); s.scroll(Scroll::Pin(s.pin(screen_point(2, 0)).unwrap())); assert_eq!(s.viewport, Viewport::Top); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); } #[test] fn page_list_scroll_to_row_0() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); assert_viewport_point(&s, 10); let target = 0; s.scroll(Scroll::Row(target)); assert_eq!(s.viewport, Viewport::Top); assert_viewport_point(&s, target); assert_scrollbar(&s, target); grow_rows(&mut s, 10); assert_viewport_point(&s, target); assert_scrollbar(&s, target); } #[test] fn page_list_scroll_to_row_in_scrollback() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 20); assert_viewport_point(&s, 20); let target = 5; s.scroll(Scroll::Row(target)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, target); assert_scrollbar(&s, target); grow_rows(&mut s, 10); assert_viewport_point(&s, target); assert_scrollbar(&s, target); } #[test] fn page_list_scroll_to_row_in_middle() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 50); let target = s.total_rows / 2; s.scroll(Scroll::Row(target)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, target); assert_scrollbar(&s, target); grow_rows(&mut s, 10); assert_viewport_point(&s, target); assert_scrollbar(&s, target); } #[test] fn page_list_scroll_to_row_at_active_boundary() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 20); s.scroll(Scroll::Row(s.total_rows - usize::from(s.rows))); assert_eq!(s.viewport, Viewport::Active); assert_viewport_point(&s, 20); assert_scrollbar(&s, 20); grow_rows(&mut s, 10); assert_eq!(s.viewport, Viewport::Active); assert_scrollbar(&s, s.total_rows - usize::from(s.rows)); } #[test] fn page_list_scroll_to_row_beyond_active() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 10); s.scroll(Scroll::Row(1000)); assert_eq!(s.viewport, Viewport::Active); assert_viewport_point(&s, 10); assert_scrollbar(&s, 10); } #[test] fn page_list_scroll_to_row_without_scrollback() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 0); s.scroll(Scroll::Row(5)); assert_eq!(s.viewport, Viewport::Active); assert_viewport_point(&s, 0); assert_scrollbar(&s, 0); } #[test] fn page_list_scroll_to_row_then_delta() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 30); for (scroll, offset) in [ (Scroll::Row(10), 10), (Scroll::DeltaRow(5), 15), (Scroll::DeltaRow(-3), 12), ] { s.scroll(scroll); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, offset); assert_scrollbar(&s, offset); } } #[test] fn page_list_scroll_to_row_with_cache_fast_path_down() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 50); s.scroll(Scroll::Row(10)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, 10); assert_scrollbar(&s, 10); assert_ne!( s.viewport_pin_row_offset.load(Ordering::Relaxed), usize::MAX ); assert_eq!(s.viewport_pin_row_offset.load(Ordering::Relaxed), 10); s.scroll(Scroll::Row(20)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, 20); assert_scrollbar(&s, 20); grow_rows(&mut s, 10); assert_viewport_point(&s, 20); assert_scrollbar(&s, 20); } #[test] fn page_list_scroll_to_row_with_cache_fast_path_up() { let mut s = PageList::init(80, 24, None).unwrap(); grow_rows(&mut s, 50); s.scroll(Scroll::Row(30)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, 30); assert_scrollbar(&s, 30); assert_ne!( s.viewport_pin_row_offset.load(Ordering::Relaxed), usize::MAX ); assert_eq!(s.viewport_pin_row_offset.load(Ordering::Relaxed), 30); s.scroll(Scroll::Row(15)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, 15); assert_scrollbar(&s, 15); grow_rows(&mut s, 10); assert_viewport_point(&s, 15); assert_scrollbar(&s, 15); } #[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 capacity = s.node(old).unwrap().data.capacity.rows; for _ in s.rows..capacity { assert!(s.grow().unwrap().is_none()); } let new = s.grow().unwrap().unwrap(); assert_eq!(s.last, Some(new)); assert_eq!(s.node(old).unwrap().next, Some(new)); let p = s.pin(active_point(0, usize::from(s.rows - 1))).unwrap(); assert_eq!(p.node, new); assert_eq!( s.point_from_pin(Tag::Screen, p), Some(screen_point(0, usize::from(capacity))) ); } #[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.viewport, Viewport::Pin); assert_eq!(s.scrollbar().offset, y); let viewport_node = s.tracked_pin(s.viewport_pin.unwrap()).unwrap().node; assert_eq!(viewport_node, old); let new = s.grow().unwrap().unwrap(); assert_eq!(s.last, Some(new)); 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.viewport, Viewport::Pin); assert_eq!(s.scrollbar().offset, y); let viewport_node = s.tracked_pin(s.viewport_pin.unwrap()).unwrap().node; assert_eq!(viewport_node, second); let new = s.grow().unwrap().unwrap(); assert_eq!(s.last, Some(new)); 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_eq!(s.tracked_pin(s.viewport_pin.unwrap()).unwrap().node, second); assert!(s.node(old).is_none()); finish(&s); } #[test] fn page_list_increase_capacity_to_increase_styles() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); let original = s.node(s.first.unwrap()).unwrap().data.capacity.styles; assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles)) .unwrap(); assert_eq!(s.first, s.last); assert_eq!( s.node(s.first.unwrap()).unwrap().data.capacity.styles, original * 2 ); for y in 0..2 { for x in 0..2 { assert_eq!( s.get_cell(active_point(x, y)).unwrap().codepoint(), u32::from(x) ); } } } #[test] fn page_list_increase_capacity_to_increase_graphemes() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); let original = s .node(s.first.unwrap()) .unwrap() .data .capacity .grapheme_bytes; assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::GraphemeBytes)) .unwrap(); assert_eq!(s.first, s.last); assert_eq!( s.node(s.first.unwrap()) .unwrap() .data .capacity .grapheme_bytes, original * 2 ); for y in 0..2 { for x in 0..2 { assert_eq!( s.get_cell(active_point(x, y)).unwrap().codepoint(), u32::from(x) ); } } } #[test] fn page_list_increase_capacity_to_increase_hyperlinks() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); let original = s .node(s.first.unwrap()) .unwrap() .data .capacity .hyperlink_bytes; assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::HyperlinkBytes)) .unwrap(); assert_eq!(s.first, s.last); assert_eq!( s.node(s.first.unwrap()) .unwrap() .data .capacity .hyperlink_bytes, original * 2 ); for y in 0..2 { for x in 0..2 { assert_eq!( s.get_cell(active_point(x, y)).unwrap().codepoint(), u32::from(x) ); } } } #[test] fn page_list_increase_capacity_to_increase_string_bytes() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); let original = s.node(s.first.unwrap()).unwrap().data.capacity.string_bytes; assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::StringBytes)) .unwrap(); assert_eq!(s.first, s.last); assert_eq!( s.node(s.first.unwrap()).unwrap().data.capacity.string_bytes, original * 2 ); for y in 0..2 { for x in 0..2 { assert_eq!( s.get_cell(active_point(x, y)).unwrap().codepoint(), u32::from(x) ); } } } #[test] fn page_list_increase_capacity_tracked_pins() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); let tracked = s.track_pin(s.pin(active_point(1, 1)).unwrap()).unwrap(); assert_eq!(s.tracked_pin(tracked).unwrap().node, s.first.unwrap()); let new = s .increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles)) .unwrap(); let p = s.tracked_pin(tracked).unwrap(); assert_eq!(p.node, new); assert_eq!(p.x, 1); assert_eq!(p.y, 1); } #[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(2, 4, Some(0)).unwrap(); let page = &mut s.node_mut(s.first.unwrap()).unwrap().data; page.dirty = true; for y in 0..4 { page.rows[y].set_dirty(y % 2 == 0); } let new = s .increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles)) .unwrap(); let page = &s.node(new).unwrap().data; assert!(page.dirty); for y in 0..4 { assert_eq!(page.rows[y].dirty(), y % 2 == 0); } } #[test] fn page_list_page_iterator_single_page() { let s = PageList::init(80, 24, None).unwrap(); assert!(s.node(s.first.unwrap()).unwrap().next.is_none()); let mut it = s.page_iterator(Direction::RightDown, active_point(0, 0), None); let chunk = it.next().unwrap(); assert_eq!(chunk.node, s.first.unwrap()); assert_eq!(chunk.start, 0); assert_eq!(chunk.end, s.rows); assert!(it.next().is_none()); } #[test] fn page_list_page_iterator_two_pages() { let mut s = PageList::init(80, 24, None).unwrap(); let capacity = s.node(s.first.unwrap()).unwrap().data.capacity.rows; for _ in s.rows..capacity { assert!(s.grow().unwrap().is_none()); } assert!(s.grow().unwrap().is_some()); let expected = [ Chunk { node: s.first.unwrap(), start: capacity - s.rows + 1, end: capacity, }, Chunk { node: s.last.unwrap(), start: 0, end: 1, }, ]; let mut it = s.page_iterator(Direction::RightDown, active_point(0, 0), None); let mut count = 0; for index in [0, 1] { let chunk = it.next().unwrap(); assert_eq!(chunk.node, expected[index].node); assert_eq!(chunk.start, expected[index].start); assert_eq!(chunk.end, expected[index].end); count += chunk.end - chunk.start; } assert!(it.next().is_none()); assert_eq!(count, s.rows); } #[test] fn page_list_cell_iterator() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let mut it = s.cell_iterator(Direction::RightDown, screen_point(0, 0), None); for (x, y) in [(0, 0), (1, 0), (0, 1), (1, 1)] { assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(x, y)) ); } assert!(it.next().is_none()); } #[test] fn page_list_page_iterator_reverse_single_page() { let s = PageList::init(80, 24, None).unwrap(); assert!(s.node(s.first.unwrap()).unwrap().next.is_none()); let mut it = s.page_iterator(Direction::LeftUp, active_point(0, 0), None); let chunk = it.next().unwrap(); assert_eq!(chunk.node, s.first.unwrap()); assert_eq!(chunk.start, 0); assert_eq!(chunk.end, s.rows); assert!(it.next().is_none()); } #[test] fn page_list_page_iterator_reverse_two_pages() { let mut s = PageList::init(80, 24, None).unwrap(); let capacity = s.node(s.first.unwrap()).unwrap().data.capacity.rows; for _ in s.rows..capacity { assert!(s.grow().unwrap().is_none()); } assert!(s.grow().unwrap().is_some()); let expected = [ Chunk { node: s.first.unwrap(), start: capacity - s.rows + 1, end: capacity, }, Chunk { node: s.last.unwrap(), start: 0, end: 1, }, ]; let mut it = s.page_iterator(Direction::LeftUp, active_point(0, 0), None); let mut count = 0; for index in [1, 0] { let chunk = it.next().unwrap(); assert_eq!(chunk.node, expected[index].node); assert_eq!(chunk.start, expected[index].start); assert_eq!(chunk.end, expected[index].end); count += chunk.end - chunk.start; } assert!(it.next().is_none()); assert_eq!(count, s.rows); } #[test] fn page_list_cell_iterator_reverse() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let mut it = s.cell_iterator(Direction::LeftUp, screen_point(0, 0), None); for (x, y) in [(1, 1), (0, 1), (1, 0), (0, 0)] { assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(x, y)) ); } assert!(it.next().is_none()); } #[test] fn page_list_resize_no_reflow_more_rows() { let mut s = PageList::init(10, 3, Some(0)).unwrap(); assert_eq!(s.total_rows, 3); grow_rows(&mut s, 0); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 0)) ); let p = s.track_pin(s.pin(active_point(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(p).unwrap()), Some(active_point(0, 2)) ); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 0)) ); } #[test] fn page_list_resize_no_reflow_more_rows_with_history() { let mut s = PageList::init(10, 3, None).unwrap(); assert_eq!(s.total_rows, 3); grow_rows(&mut s, 50); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 50)) ); let p = s.track_pin(s.pin(active_point(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(p).unwrap()), Some(active_point(0, 4)) ); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 48)) ); } #[test] fn page_list_resize_no_reflow_less_rows() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); assert_eq!(s.total_rows, 10); assert_eq!(s.first, s.last); for y in 0..10 { set_raw(&mut s, 0, y, 'A' as u32, Wide::Narrow); } 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.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 5)) ); } #[test] fn page_list_resize_no_reflow_one_rows() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); assert_eq!(s.total_rows, 10); assert_eq!(s.first, s.last); for y in 0..10 { set_raw(&mut s, 0, y, 'A' as u32, Wide::Narrow); } 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.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 9)) ); } #[test] fn page_list_resize_no_reflow_less_cols() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); s.resize(Resize { cols: Some(5), rows: Some(10), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); assert_eq!(s.rows, 10); assert_eq!(s.total_rows, 10); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 5); } } #[test] fn page_list_resize_no_reflow_less_cols_pin_in_trimmed_cols() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); let p = s.track_pin(s.pin(active_point(8, 2)).unwrap()).unwrap(); s.resize(Resize { cols: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); assert_eq!(s.total_rows, 10); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 5); } assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(4, 2)) ); } #[test] fn page_list_resize_no_reflow_more_cols() { let mut s = PageList::init(5, 3, Some(0)).unwrap(); s.resize(Resize { cols: Some(10), rows: Some(3), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 10); assert_eq!(s.rows, 3); assert_eq!(s.total_rows, 3); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 10); } } #[test] fn page_list_resize_no_reflow_less_cols_then_more_cols() { let mut s = PageList::init(5, 3, Some(0)).unwrap(); s.resize(Resize { cols: Some(2), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); s.resize(Resize { cols: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); assert_eq!(s.total_rows, 3); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 5); } } #[test] fn page_list_resize_reflow_more_cols_no_wrapped_rows() { let mut s = PageList::init(5, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in 0..3 { for x in 0..5 { set_raw(&mut s, x, y, 'A' as u32, Wide::Narrow); } } 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_point(0, 0), None) { let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), 10); assert_eq!(cells[0].codepoint(), 'A' as u32); } } #[test] fn page_list_resize_reflow_more_cols_wrapped_rows() { let mut s = PageList::init(2, 4, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in 0..4 { set_wrap(&mut s, y, y % 2 == 0, y % 2 != 0); for x in 0..2 { set_raw(&mut s, x, y, 'A' as u32, Wide::Narrow); } } s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 4); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 0)) ); let p = s .row_iterator(Direction::RightDown, screen_point(0, 0), None) .next() .unwrap(); assert!(!p.row_and_cell(&s).unwrap().0.wrap()); let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), 4); assert_eq!(cells[0].codepoint(), 'A' as u32); assert_eq!(cells[2].codepoint(), 'A' as u32); } #[test] fn page_list_resize_reflow_less_cols_no_wrapped_rows() { let mut s = PageList::init(10, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in 0..3 { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.resize(Resize { cols: Some(5), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); assert_eq!(s.total_rows, 3); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { let cells = p.cells(&s, CellSubset::All).unwrap(); for x in 0..4 { assert_eq!(cells.len(), 5); assert_eq!(cells[x].codepoint(), x as u32); } } } #[test] fn page_list_resize_reflow_less_cols_wrapped_rows() { let mut s = PageList::init(4, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 2)) ); assert_eq!(s.first, s.last); let mut it = s.row_iterator(Direction::RightDown, screen_point(0, 0), None); for y in 0..4 { let p = it.next().unwrap(); assert_eq!(p.row_and_cell(&s).unwrap().0.wrap(), y % 2 == 0); let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), 2); assert_eq!(cells[0].codepoint(), if y % 2 == 0 { 0 } else { 2 }); } } #[test] fn page_list_reset() { let mut s = PageList::init(80, 24, None).unwrap(); s.reset().unwrap(); assert_eq!(s.viewport, Viewport::Active); assert_eq!(s.total_rows, 24); assert_eq!( s.get_top_left(Tag::Active), Some(Pin { node: s.first.unwrap(), y: 0, x: 0, garbage: false }) ); finish(&s); } #[test] fn page_list_reset_across_two_pages() { let rows = 100; let mut cols = 50; while initial_capacity(cols).rows >= rows { cols += 50; } let mut s = PageList::init(cols, rows, None).unwrap(); s.reset().unwrap(); assert_eq!(s.viewport, Viewport::Active); assert!(s.first.is_some()); assert_eq!(s.total_rows, usize::from(s.rows)); 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), Some(Pin { node: s.first.unwrap(), y: 0, x: 0, garbage: false }) ); 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); } assert_eq!(s.node(s.last.unwrap()).unwrap().data.size.rows, 5); 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); } assert_eq!(s.node(s.last.unwrap()).unwrap().data.size.rows, 1); 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(4, 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, 4); finish(&s); } #[test] fn page_list_split_multiple_tracked_pins_across_regions() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); let coords = [(0, 1), (2, 5), (3, 7), (8, 9)]; let pins = coords.map(|(x, y)| s.track_pin(s.pin(screen(x, y)).unwrap()).unwrap()); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); for ((x, y), handle) in coords.into_iter().zip(pins) { let p = s.tracked_pin(handle).unwrap(); assert_eq!( p.node, if y < 5 { s.first.unwrap() } else { s.last.unwrap() } ); assert_eq!(p.y, if y < 5 { y as u16 } else { y as u16 - 5 }); assert_eq!(p.x, x); } finish(&s); } #[test] fn page_list_split_tracked_viewport_pin_in_split_region_moves_correctly() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); s.set_tracked_pin(s.viewport_pin.unwrap(), s.pin(screen(6, 7)).unwrap()) .unwrap(); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); let p = s.tracked_pin(s.viewport_pin.unwrap()).unwrap(); assert_eq!(p.node, s.last.unwrap()); assert_eq!(p.y, 2); assert_eq!(p.x, 6); finish(&s); } #[test] fn page_list_split_preserves_styled_cells() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); let page = &mut s.node_mut(s.first.unwrap()).unwrap().data; let style = crate::style::Style { flags: crate::style::Flags { bold: true, ..Default::default() }, ..Default::default() }; let id = page.styles.add(style).unwrap(); for y in 5..8 { let i = page.cell_index(0, y).unwrap(); page.cells[i] = Cell::init('S' as u32); page.cells[i].set_style_id(id); page.rows[y].set_styled(true); page.styles.r#use(id).unwrap(); } page.styles.release(id).unwrap(); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); assert_eq!(s.node(s.first.unwrap()).unwrap().data.styles.count(), 0); let page = &s.node(s.last.unwrap()).unwrap().data; assert_eq!(page.styles.count(), 1); for y in 0..3 { let (row, cell) = page.get_row_and_cell(0, y).unwrap(); assert_eq!(cell.codepoint(), 'S' as u32); assert_ne!(cell.style_id(), 0); assert!(page.styles.get(cell.style_id()).unwrap().flags.bold); assert!(row.styled()); } finish(&s); } #[test] fn page_list_split_preserves_grapheme_clusters() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); set_raw(&mut s, 0, 6, 0x1f468, Wide::Narrow); s.node_mut(s.first.unwrap()) .unwrap() .data .set_graphemes(0, 6, &[0x200d, 0x1f469]) .unwrap(); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); assert_eq!(s.node(s.first.unwrap()).unwrap().data.grapheme_count(), 0); let page = &s.node(s.last.unwrap()).unwrap().data; assert_eq!(page.grapheme_count(), 1); let (row, cell) = page.get_row_and_cell(0, 1).unwrap(); assert_eq!(cell.codepoint(), 0x1f468); assert!(row.grapheme()); assert_eq!( page.lookup_grapheme(page.cell_index(0, 1).unwrap()) .unwrap(), [0x200d, 0x1f469] ); finish(&s); } #[test] fn page_list_split_preserves_hyperlinks() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); let page = &mut s.node_mut(s.first.unwrap()).unwrap().data; let id = page .insert_hyperlink(crate::hyperlink::Hyperlink { id: crate::hyperlink::HyperlinkId::Implicit(0), uri: b"https://example.com".to_vec(), }) .unwrap(); let i = page.cell_index(0, 7).unwrap(); page.cells[i] = Cell::init('L' as u32); page.set_hyperlink(0, 7, id).unwrap(); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); assert_eq!( s.node(s.first.unwrap()).unwrap().data.hyperlink_set.count(), 0 ); let page = &s.node(s.last.unwrap()).unwrap().data; assert_eq!(page.hyperlink_set.count(), 1); let (_, cell) = page.get_row_and_cell(0, 2).unwrap(); assert_eq!(cell.codepoint(), 'L' as u32); assert!(cell.hyperlink()); let id = page .lookup_hyperlink(page.cell_index(0, 2).unwrap()) .unwrap(); assert_eq!( page.hyperlink_set.get(id).unwrap().uri, b"https://example.com" ); finish(&s); } #[test] fn page_list_split_preserves_wrap_flags() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); let page = &mut s.node_mut(s.first.unwrap()).unwrap().data; page.rows[5].set_wrap(true); page.rows[6].set_wrap_continuation(true); page.rows[7].set_wrap(true); page.rows[7].set_wrap_continuation(true); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); let page = &s.node(s.last.unwrap()).unwrap().data; for (y, (wrap, continuation)) in [(true, false), (false, true), (true, true)] .into_iter() .enumerate() { assert_eq!(page.rows[y].wrap(), wrap); assert_eq!(page.rows[y].wrap_continuation(), continuation); } finish(&s); } #[test] fn page_list_split_middle_page_preserves_linked_list_order() { let mut s = PageList::init(10, 12, Some(0)).unwrap(); s.split(s.pin(screen(0, 4)).unwrap()).unwrap(); let first = s.first.unwrap(); let middle = s.last.unwrap(); assert_eq!(s.node(first).unwrap().data.size.rows, 4); assert_eq!(s.node(middle).unwrap().data.size.rows, 8); s.split(Pin { node: middle, x: 0, y: 4, garbage: false, }) .unwrap(); let last = s.node(middle).unwrap().next.unwrap(); assert_eq!(s.first, Some(first)); assert_eq!(s.node(first).unwrap().next, Some(middle)); assert_eq!(s.last, Some(last)); assert_eq!(s.node(first).unwrap().prev, None); assert_eq!(s.node(middle).unwrap().prev, Some(first)); assert_eq!(s.node(last).unwrap().prev, Some(middle)); assert_eq!(s.node(middle).unwrap().next, Some(last)); assert_eq!(s.node(last).unwrap().next, None); for id in [first, middle, last] { assert_eq!(s.node(id).unwrap().data.size.rows, 4); } finish(&s); } #[test] fn page_list_split_last_page_makes_new_page_the_last() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); let old = s.last.unwrap(); assert_eq!(s.node(old).unwrap().data.size.rows, 5); s.split(Pin { node: old, x: 0, y: 2, garbage: false, }) .unwrap(); let new = s.last.unwrap(); assert_ne!(new, old); assert_eq!(s.node(new).unwrap().prev, Some(old)); assert_eq!(s.node(new).unwrap().next, None); assert_eq!(s.node(old).unwrap().data.size.rows, 2); assert_eq!(s.node(new).unwrap().data.size.rows, 3); finish(&s); } #[test] fn page_list_split_first_page_keeps_original_as_first() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); let first = s.first.unwrap(); s.split(s.pin(screen(0, 5)).unwrap()).unwrap(); let second = s.last.unwrap(); s.split(s.pin(screen(0, 2)).unwrap()).unwrap(); assert_eq!(s.first, Some(first)); assert_eq!(s.node(first).unwrap().prev, None); let inserted = s.node(first).unwrap().next.unwrap(); assert_ne!(inserted, second); assert_eq!(s.node(inserted).unwrap().next, Some(second)); assert_eq!(s.node(first).unwrap().data.size.rows, 2); assert_eq!(s.node(inserted).unwrap().data.size.rows, 3); assert_eq!(s.node(second).unwrap().data.size.rows, 5); finish(&s); } #[test] fn page_list_compact_std_size_page_returns_null() { let mut s = PageList::init(80, 24, Some(0)).unwrap(); let node = s.first.unwrap(); assert!(Page::layout(s.node(node).unwrap().data.capacity).total_size <= std_size()); assert_eq!(s.compact(node).unwrap(), None); assert_eq!(s.first, Some(node)); finish(&s); } #[test] fn page_list_compact_oversized_page() { let mut s = PageList::init(80, 24, None).unwrap(); let additional = first_cap(&s) - s.total_rows; grow(&mut s, additional + 1); assert_ne!(s.first, s.last); let mut node = s.first.unwrap(); let page = &mut s.node_mut(node).unwrap().data; for y in 0..usize::from(page.size.rows) { for x in 0..80 { let i = page.cell_index(x, y).unwrap(); page.cells[i] = Cell::init((x + y * 80) as u32); } } let tracked = s .track_pin(Pin { node, x: 5, y: 10, garbage: false, }) .unwrap(); while Page::layout(s.node(node).unwrap().data.capacity).total_size <= std_size() { node = s .increase_capacity(node, Some(IncreaseCapacity::GraphemeBytes)) .unwrap(); } let oversized_len = Page::layout(s.node(node).unwrap().data.capacity).total_size; assert!(oversized_len > std_size()); let original_size = s.node(node).unwrap().data.size; let second = s.node(node).unwrap().next.unwrap(); s.node_mut(node).unwrap().data.dirty = true; let new = s.compact(node).unwrap().unwrap(); let page = &s.node(new).unwrap().data; assert!(Page::layout(page.capacity).total_size < oversized_len); assert_eq!(page.size, original_size); assert!(page.dirty); assert_eq!(s.first, Some(new)); assert_eq!(s.node(new).unwrap().prev, None); assert_eq!(s.node(new).unwrap().next, Some(second)); assert_eq!(s.node(second).unwrap().prev, Some(new)); let pin = s.tracked_pin(tracked).unwrap(); assert_eq!((pin.node, pin.x, pin.y), (new, 5, 10)); for y in 0..usize::from(page.size.rows) { for x in 0..80 { assert_eq!( page.get_row_and_cell(x, y).unwrap().1.codepoint(), (x + y * 80) as u32 ); } } finish(&s); } #[test] fn page_list_clone_partial_trimmed_right() { let mut s = PageList::init(80, 20, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 30); let (s2, _) = s .clone_range(screen_point(0, 0), Some(screen_point(0, 39))) .unwrap(); assert_eq!(s2.total_rows, 40); } #[test] fn page_list_clone_partial_trimmed_left() { let mut s = PageList::init(80, 20, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 30); let (s2, _) = s.clone_range(screen_point(0, 10), None).unwrap(); assert_eq!(s2.total_rows, 40); } #[test] fn page_list_clone_partial_trimmed_both() { let mut s = PageList::init(80, 20, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 30); let (s2, _) = s .clone_range(screen_point(0, 10), Some(screen_point(0, 35))) .unwrap(); assert_eq!(s2.total_rows, 26); } #[test] fn page_list_clone() { let mut s = PageList::init(80, 24, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 0); let (s2, _) = s.clone_range(screen_point(0, 0), None).unwrap(); assert_eq!(s2.total_rows, 24); } #[test] fn page_list_clone_less_than_active() { let mut s = PageList::init(80, 24, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 0); let (s2, _) = s.clone_range(active_point(0, 5), None).unwrap(); assert_eq!(s2.total_rows, 24); } #[test] fn page_list_clone_remap_tracked_pin() { let mut s = PageList::init(80, 24, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); 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(); assert_eq!(s.total_rows, usize::from(s.rows)); 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(); assert_eq!(s.total_rows, usize::from(s.rows)); for y in [0, 12, 23] { s.pin(active_point(0, y)).unwrap().mark_dirty(&mut s); } let (s2, _) = s.clone_range(screen_point(0, 0), None).unwrap(); assert_eq!(s2.total_rows, usize::from(s.rows)); for (y, dirty) in [(0, true), (1, false), (12, true), (14, false), (23, true)] { assert_eq!(s2.is_dirty(active_point(0, y)), dirty); } } #[test] fn page_list_clone_partial_trimmed_left_reclaims_styles() { use crate::style::{Flags, Style}; let mut s = PageList::init(80, 20, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); grow_rows(&mut s, 30); assert_eq!(s.first, s.last); let page = &mut s.node_mut(s.first.unwrap()).unwrap().data; let style = Style { flags: Flags { bold: true, ..Flags::default() }, ..Style::default() }; let id = page.styles.add(style).unwrap(); for y in (0..10).rev() { let i = page.cell_index(0, y).unwrap(); page.cells[i] = Cell::init('A' as u32); page.cells[i].set_style_id(id); page.rows[y].set_styled(true); page.styles.r#use(id).unwrap(); } page.styles.release(id).unwrap(); assert_eq!(page.styles.count(), 1); let (s2, _) = s.clone_range(screen_point(0, 10), None).unwrap(); assert_eq!(s2.total_rows, 40); assert_eq!(s2.first, s2.last); assert_eq!(s2.node(s2.first.unwrap()).unwrap().data.styles.count(), 0); } #[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 handle = s.track_pin(s.pin(active_point(2, 4)).unwrap()).unwrap(); s.erase_rows(active_point(0, 0), Some(active_point(0, 3))) .unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); let p = s.tracked_pin(handle).unwrap(); assert_eq!(p.node, s.first.unwrap()); assert_eq!(p.y, 0); assert_eq!(p.x, 2); } #[test] fn page_list_erase_row_with_tracked_pin_is_erased() { let mut s = PageList::init(80, 24, None).unwrap(); let handle = s.track_pin(s.pin(active_point(2, 2)).unwrap()).unwrap(); s.erase_rows(active_point(0, 0), Some(active_point(0, 3))) .unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); let p = s.tracked_pin(handle).unwrap(); assert_eq!(p.node, s.first.unwrap()); assert_eq!(p.y, 0); assert_eq!(p.x, 0); } #[test] fn page_list_erase_active_regrows_automatically() { let mut s = PageList::init(80, 24, None).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); s.erase_rows(active(0, 0), Some(active(0, 10))).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); finish(&s); } #[test] fn page_list_erase_a_one_row_active() { let mut s = PageList::init(10, 1, None).unwrap(); assert_eq!(s.total_pages(), 1); set_raw(&mut s, 0, 0, 'A' as u32, Wide::Narrow); s.erase_rows(active_point(0, 0), Some(active_point(0, 0))) .unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); assert_eq!(s.get_cell(active_point(0, 0)).unwrap().codepoint(), 0); } #[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_point(0, y)).unwrap()).unwrap()) .collect(); s.erase_row_bounded(active_point(0, 5), 3).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); for y in 5..8 { assert!(s.is_dirty(active_point(0, y))); } for (h, y) in handles.into_iter().zip([4, 7, 9]) { let p = s.tracked_pin(h).unwrap(); assert_eq!(p.node, s.first.unwrap()); assert_eq!(p.y, y); assert_eq!(p.x, 0); } } #[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_point(5, 0)).unwrap()).unwrap(); s.erase_row_bounded(active_point(0, 0), 3).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); for y in 0..3 { assert!(s.is_dirty(active_point(0, y))); } let p = s.tracked_pin(h).unwrap(); assert_eq!(p.node, s.first.unwrap()); assert_eq!(p.y, 0); assert_eq!(p.x, 0); } #[test] fn page_list_erase_row_bounded_full_rows_single_page() { let mut s = PageList::init(80, 10, None).unwrap(); let handles: Vec<_> = [7, 9] .into_iter() .map(|y| s.track_pin(s.pin(active_point(0, y)).unwrap()).unwrap()) .collect(); s.erase_row_bounded(active_point(0, 5), 10).unwrap(); assert_eq!(s.total_rows, usize::from(s.rows)); for y in 5..10 { assert!(s.is_dirty(active_point(0, y))); } for (h, y) in handles.into_iter().zip([6, 8]) { let p = s.tracked_pin(h).unwrap(); assert_eq!(p.node, s.first.unwrap()); assert_eq!(p.y, y); assert_eq!(p.x, 0); } } #[test] fn page_list_resize_reflow_less_cols_cursor_in_unchanged_row() { let mut s = PageList::init(4, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s.track_pin(s.pin(active_point(1, 0)).unwrap()).unwrap(); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 2); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(1, 0)) ); } #[test] fn page_list_resize_reflow_less_cols_cursor_in_blank_cell() { let mut s = PageList::init(6, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s.track_pin(s.pin(active_point(2, 0)).unwrap()).unwrap(); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 2); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(2, 0)) ); } #[test] fn page_list_resize_reflow_less_cols_cursor_in_final_blank_cell() { let mut s = PageList::init(6, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s.track_pin(s.pin(active_point(3, 0)).unwrap()).unwrap(); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 2); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(3, 0)) ); } #[test] fn page_list_resize_reflow_less_cols_cursor_in_wrapped_blank_cell() { let mut s = PageList::init(6, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s.track_pin(s.pin(active_point(5, 0)).unwrap()).unwrap(); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 2); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(3, 0)) ); } #[test] fn page_list_resize_reflow_less_cols_cursor_in_wrapped_row() { let mut s = PageList::init(4, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s.track_pin(s.pin(active_point(2, 1)).unwrap()).unwrap(); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(0, 1)) ); } #[test] fn page_list_resize_reflow_less_cols_blank_lines() { let mut s = PageList::init(4, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in [0] { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 3); let mut it = s.row_iterator(Direction::RightDown, active_point(0, 0), None); for (wrap, cp) in [(true, 0), (false, 2)] { let p = it.next().unwrap(); assert_eq!(p.row_and_cell(&s).unwrap().0.wrap(), wrap); let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), 2); assert_eq!(cells[0].codepoint(), cp); } } #[test] fn page_list_resize_reflow_less_cols_blank_lines_between() { let mut s = PageList::init(4, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in [0, 2] { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 5); let mut it = s.row_iterator(Direction::RightDown, active_point(0, 0), None); assert!(!it.next().unwrap().row_and_cell(&s).unwrap().0.wrap()); for (wrap, cp) in [(true, 0), (false, 2)] { let p = it.next().unwrap(); assert_eq!(p.row_and_cell(&s).unwrap().0.wrap(), wrap); let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), 2); assert_eq!(cells[0].codepoint(), cp); } } #[test] fn page_list_resize_reflow_less_cols_blank_lines_between_no_scrollback() { let mut s = PageList::init(5, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, 'A' as u32, Wide::Narrow); set_raw(&mut s, 0, 2, 'C' as u32, Wide::Narrow); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 3); let mut it = s.row_iterator(Direction::RightDown, active_point(0, 0), None); for cp in ['A' as u32, 0, 'C' as u32] { let p = it.next().unwrap(); let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells[0].codepoint(), cp); if cp != 0 { assert!(!p.row_and_cell(&s).unwrap().0.wrap()); assert_eq!(cells.len(), 2); } } } #[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(); assert_eq!(s.first, s.last); for y in 0..5 { for x in 0..2 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } grow_rows(&mut s, 5); assert_eq!(s.total_rows, 10); let p = s.track_pin(s.pin(active_point(0, 0)).unwrap()).unwrap(); s.resize(Resize { cols: Some(4), cursor: Some(Cursor { x: 1, y: 1 }), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 10); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(0, 0)) ); } #[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(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 0, SemanticPrompt::Prompt); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert_eq!(s.first, s.last); assert_eq!( s.pin(active_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .semantic_prompt(), SemanticPrompt::Prompt ); } #[test] fn page_list_resize_reflow_less_cols_wrap_preserves_semantic_prompt() { let mut s = PageList::init(4, 4, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 0, SemanticPrompt::Prompt); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert_eq!(s.first, s.last); assert_eq!( s.pin(active_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .semantic_prompt(), SemanticPrompt::Prompt ); } #[test] fn page_list_resize_reflow_less_cols_no_reflow_preserves_semantic_prompt() { let mut s = PageList::init(4, 4, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 1, SemanticPrompt::Prompt); for x in 0..4 { set_raw(&mut s, x, 1, u32::from(x), Wide::Narrow); } s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert_eq!(s.first, s.last); assert_eq!( s.pin(active_point(0, 1)) .unwrap() .row_and_cell(&s) .unwrap() .0 .semantic_prompt(), SemanticPrompt::Prompt ); assert!( s.pin(active_point(0, 1)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); assert_eq!( s.pin(active_point(0, 2)) .unwrap() .row_and_cell(&s) .unwrap() .0 .semantic_prompt(), SemanticPrompt::Prompt ); } #[test] fn page_list_resize_reflow_less_cols_to_wrap_a_wide_char() { let mut s = PageList::init(3, 1, Some(0)).unwrap(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, 'x' as u32, Wide::Narrow); set_raw(&mut s, 1, 0, '😀' as u32, Wide::Wide); set_raw(&mut s, 2, 0, 0, Wide::SpacerTail); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 2); assert_eq!(s.first, s.last); assert_cell(&s, 0, 0, 'x' as u32, Wide::Narrow); assert!( s.pin(screen_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); assert_cell(&s, 1, 0, 0, Wide::SpacerHead); assert_cell(&s, 0, 1, '😀' as u32, Wide::Wide); assert_cell(&s, 1, 1, 0, Wide::SpacerTail); } #[test] fn page_list_resize_reflow_more_cols_unwrap_still_requires_wide_spacer_head() { let mut s = PageList::init(2, 2, Some(0)).unwrap(); assert_eq!(s.first, s.last); for x in 0..2 { set_raw(&mut s, x, 0, 'x' as u32, Wide::Narrow); } set_wrap(&mut s, 0, true, false); set_raw(&mut s, 0, 1, '😀' as u32, Wide::Wide); set_raw(&mut s, 1, 1, 0, Wide::SpacerTail); set_wrap(&mut s, 1, false, true); s.resize(Resize { cols: Some(3), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 3); assert_eq!(s.total_rows, 2); assert_eq!(s.first, s.last); assert!( s.pin(screen_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); for x in 0..2 { assert_cell(&s, x, 0, 'x' as u32, Wide::Narrow); } assert_cell(&s, 2, 0, 0, Wide::SpacerHead); assert_cell(&s, 0, 1, '😀' as u32, Wide::Wide); assert_cell(&s, 1, 1, 0, Wide::SpacerTail); } #[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(); assert_eq!(s.first, s.last); let cps = [0x200D, 0x1F468, 0x200D, 0x1F466, 0x200D, 0x1F466]; for x in [0, 2] { set_raw(&mut s, x, 0, 0x1F468, Wide::Wide); s.node_mut(s.first.unwrap()) .unwrap() .data .set_graphemes(usize::from(x), 0, &cps) .unwrap(); set_raw(&mut s, x + 1, 0, 0, Wide::SpacerTail); } s.resize(Resize { cols: Some(3), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 3); assert_eq!(s.total_rows, 2); assert_eq!(s.first, s.last); // Zig checks the first cluster twice; also check the moved second one. for y in [0, 0, 1] { assert_cell(&s, 0, y, 0x1F468, Wide::Wide); let p = s.pin(screen_point(0, y)).unwrap(); assert_eq!(p.grapheme(&s).unwrap().len(), 6); assert_eq!(p.grapheme(&s).unwrap(), cps); } assert!( s.pin(screen_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); assert_cell(&s, 1, 0, 0, Wide::SpacerTail); assert_cell(&s, 2, 0, 0, Wide::SpacerHead); assert_cell(&s, 1, 1, 0, Wide::SpacerTail); } #[test] fn page_list_prompt_iterator_left_up() { let mut s = PageList::init(2, 20, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in [3, 6] { mark_prompt(&mut s, y, SemanticPrompt::Prompt); } for y in [7, 8, 12] { mark_prompt(&mut s, y, SemanticPrompt::PromptContinuation); } let mut it = s.prompt_iterator(Direction::LeftUp, screen_point(0, 0), None); for y in [12, 6, 3] { assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(0, y)) ); } assert!(it.next().is_none()); } #[test] fn page_list_prompt_iterator_right_down() { let mut s = PageList::init(2, 20, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in [3, 6] { mark_prompt(&mut s, y, SemanticPrompt::Prompt); } for y in [7, 8, 12] { mark_prompt(&mut s, y, SemanticPrompt::PromptContinuation); } let mut it = s.prompt_iterator(Direction::RightDown, screen_point(0, 0), None); for y in [3, 6, 12] { assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(0, y)) ); } assert!(it.next().is_none()); } #[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(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, 'A' as u32, Wide::Narrow); for y in 1..usize::from(s.rows) { let c = s.get_cell_mut(active_point(0, y)).unwrap(); c.set_content_tag(ContentTag::BgColorRgb); c.set_color_rgb(ghostty_vt::color::Rgb { r: 255, g: 0, b: 0 }); } let p = s.track_pin(s.pin(active_point(0, 0)).unwrap()).unwrap(); let pt = s .point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()) .unwrap(); assert_eq!(s.get_cell(pt).unwrap().codepoint(), 'A' as u32); s.resize(Resize { rows: Some(2), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.rows, 2); assert_eq!(s.total_rows, 2); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(0, 0)) ); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 0)) ); } #[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(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, 'A' as u32, Wide::Narrow); for y in 1..usize::from(s.rows) { let c = s.get_cell_mut(active_point(0, y)).unwrap(); c.set_content_tag(ContentTag::BgColorRgb); c.set_color_rgb(ghostty_vt::color::Rgb { r: 255, g: 0, b: 0 }); } let p = s.track_pin(s.pin(active_point(0, 3)).unwrap()).unwrap(); s.resize(Resize { rows: Some(2), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.rows, 2); assert_eq!(s.total_rows, 4); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(0, 1)) ); } #[test] fn page_list_resize_no_reflow_less_rows_trims_blank_lines_erases_pages() { let mut s = PageList::init(100, 5, Some(0)).unwrap(); assert_eq!(s.first, s.last); let rows = s.node(s.first.unwrap()).unwrap().data.capacity.rows + 10; s.resize(Resize { rows: Some(rows), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.total_pages(), 2); set_raw(&mut s, 0, 0, 'A' as u32, Wide::Narrow); s.resize(Resize { rows: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.rows, 5); assert_eq!(s.total_rows, 5); assert_eq!(s.total_pages(), 1); } #[test] fn page_list_resize_no_reflow_more_rows_extends_blank_lines() { let mut s = PageList::init(10, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, 'A' as u32, Wide::Narrow); for y in 1..usize::from(s.rows) { let c = s.get_cell_mut(active_point(0, y)).unwrap(); c.set_content_tag(ContentTag::BgColorRgb); c.set_color_rgb(ghostty_vt::color::Rgb { r: 255, g: 0, b: 0 }); } s.resize(Resize { rows: Some(7), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.rows, 7); assert_eq!(s.total_rows, 7); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 0)) ); } #[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('A' as u32); p.append_grapheme(9, 0, 'A' as u32).unwrap(); assert_eq!(p.grapheme_count(), 1); s.resize(Resize { cols: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); 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); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 5); } } #[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); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 5); } } #[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); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { assert_eq!(p.cells(&s, CellSubset::All).unwrap().len(), 10); } } #[test] fn page_list_increase_capacity_after_col_shrink() { let mut s = PageList::init(10, 2, Some(0)).unwrap(); s.resize(Resize { cols: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); assert_eq!(s.node(s.first.unwrap()).unwrap().data.size.cols, 5); assert!(s.node(s.first.unwrap()).unwrap().data.capacity.cols >= 10); s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles)) .unwrap(); assert_eq!(s.node(s.first.unwrap()).unwrap().data.size.cols, 5); assert_eq!(s.cols, 5); } #[test] fn page_list_increase_capacity_multi_page() { let mut s = PageList::init(80, 24, None).unwrap(); let capacity = s.node(s.first.unwrap()).unwrap().data.capacity.rows; for _ in s.rows..capacity { assert!(s.grow().unwrap().is_none()); } assert!(s.grow().unwrap().is_some()); assert_ne!(s.first, s.last); let before_first = s.node(s.first.unwrap()).unwrap().data.capacity.styles; let before_last = s.node(s.last.unwrap()).unwrap().data.capacity.styles; s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::Styles)) .unwrap(); assert_eq!( s.node(s.first.unwrap()).unwrap().data.capacity.styles, before_first * 2 ); assert_eq!( s.node(s.last.unwrap()).unwrap().data.capacity.styles, before_last ); } #[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 bytes <= std_size() { assert_eq!(result, None); } else 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 capacity = s.node(s.first.unwrap()).unwrap().data.capacity.rows; for _ in s.rows..capacity { assert!(s.grow().unwrap().is_none()); } assert!(s.grow().unwrap().is_some()); let mut it = s.page_iterator( Direction::RightDown, Point::History(Coordinate::default()), None, ); let chunk = it.next().unwrap(); assert_eq!(chunk.node, s.first.unwrap()); assert_eq!(chunk.start, 0); assert_eq!(chunk.end, s.get_top_left(Tag::Active).unwrap().y); assert!(it.next().is_none()); } #[test] fn page_list_page_iterator_reverse_history_two_pages() { let mut s = PageList::init(80, 24, None).unwrap(); let capacity = s.node(s.first.unwrap()).unwrap().data.capacity.rows; for _ in s.rows..capacity { assert!(s.grow().unwrap().is_none()); } assert!(s.grow().unwrap().is_some()); let mut it = s.page_iterator( Direction::LeftUp, Point::History(Coordinate::default()), None, ); let chunk = it.next().unwrap(); assert_eq!(chunk.node, s.first.unwrap()); assert_eq!(chunk.start, 0); assert_eq!(chunk.end, s.get_top_left(Tag::Active).unwrap().y); assert!(it.next().is_none()); } #[test] fn page_list_erase_row_bounded_full_rows_two_pages() { let mut s = PageList::init(80, 10, None).unwrap(); let capacity = s.node(s.first.unwrap()).unwrap().data.capacity.rows; grow_rows(&mut s, usize::from(capacity - 10) + 5); assert_eq!(s.total_pages(), 2); assert_eq!(s.node(s.last.unwrap()).unwrap().data.size.rows, 5); let handles: Vec<_> = [4, 3, 8, 9] .into_iter() .map(|y| s.track_pin(s.pin(active_point(0, y)).unwrap()).unwrap()) .collect(); for (h, (node, y)) in handles.iter().zip([ (s.first.unwrap(), capacity - 1), (s.first.unwrap(), capacity - 2), (s.last.unwrap(), 3), (s.last.unwrap(), 4), ]) { let p = s.tracked_pin(*h).unwrap(); assert_eq!(p.node, node); assert_eq!(p.y, y); assert_eq!(p.x, 0); } s.erase_row_bounded(active_point(0, 4), 4).unwrap(); for y in 4..8 { assert!(s.is_dirty(active_point(0, y))); } for (h, (node, y)) in handles.iter().zip([ (s.first.unwrap(), capacity - 2), (s.first.unwrap(), capacity - 2), (s.last.unwrap(), 2), (s.last.unwrap(), 4), ]) { let p = s.tracked_pin(*h).unwrap(); assert_eq!(p.node, node); assert_eq!(p.y, y); assert_eq!(p.x, 0); } } #[test] fn page_list_resize_reflow_less_cols_wrapped_rows_with_graphemes() { let mut s = PageList::init(4, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } s.node_mut(s.first.unwrap()) .unwrap() .data .append_grapheme(2, y, 'A' as u32) .unwrap(); } s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 2)) ); assert_eq!(s.first, s.last); let mut it = s.row_iterator(Direction::RightDown, screen_point(0, 0), None); for y in 0..4 { let p = it.next().unwrap(); assert_eq!(p.row_and_cell(&s).unwrap().0.wrap(), y % 2 == 0); let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), 2); assert_eq!(cells[0].codepoint(), if y % 2 == 0 { 0 } else { 2 }); if y % 2 != 0 { assert!(p.row_and_cell(&s).unwrap().0.grapheme()); let cps = p.grapheme(&s).unwrap(); assert_eq!(cps.len(), 1); assert_eq!(cps[0], 'A' as u32); } } } #[test] fn page_list_resize_reflow_less_cols_copy_style() { use crate::style::{Flags, Style}; let mut s = PageList::init(4, 2, Some(0)).unwrap(); assert_eq!(s.first, s.last); let page = &mut s.node_mut(s.first.unwrap()).unwrap().data; let id = page .styles .add(Style { flags: Flags { bold: true, ..Flags::default() }, ..Style::default() }) .unwrap(); for x in 0..3 { page.get_cells_mut(0).unwrap()[x] = Cell::init(x as u32); page.get_cells_mut(0).unwrap()[x].set_style_id(id); page.styles.r#use(id).unwrap(); } page.styles.release(id).unwrap(); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 2); for p in s.row_iterator(Direction::RightDown, active_point(0, 0), None) { for x in 0..s.cols - 1 { let p = Pin { x, ..p }; let (row, cell) = p.row_and_cell(&s).unwrap(); assert_ne!(cell.style_id(), 0); assert!(p.style(&s).unwrap().flags.bold); assert!(row.styled()); } } } #[test] fn page_list_resize_reflow_less_cols_wraps_spacer_head() { let mut s = PageList::init(4, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); for x in 0..3 { set_raw(&mut s, x, 0, 'x' as u32, Wide::Narrow); } set_raw(&mut s, 3, 0, 0, Wide::SpacerHead); set_wrap(&mut s, 0, true, false); set_raw(&mut s, 0, 1, '😀' as u32, Wide::Wide); set_raw(&mut s, 1, 1, 0, Wide::SpacerTail); set_wrap(&mut s, 1, false, true); s.resize(Resize { cols: Some(3), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 3); assert_eq!(s.total_rows, 3); assert_eq!(s.first, s.last); assert!( s.pin(screen_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); for x in 0..3 { assert_cell(&s, x, 0, 'x' as u32, Wide::Narrow); } assert_cell(&s, 0, 1, '😀' as u32, Wide::Wide); assert_cell(&s, 1, 1, 0, Wide::SpacerTail); } #[test] fn page_list_resize_reflow_more_cols_no_reflow_preserves_semantic_prompt() { let mut s = PageList::init(2, 4, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 1, SemanticPrompt::Prompt); s.resize(Resize { cols: Some(4), reflow: true, ..Default::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 4); assert_eq!(s.first, s.last); assert_eq!( s.node(s.first.unwrap()).unwrap().data.rows[1].semantic_prompt(), SemanticPrompt::Prompt ); } #[test] fn page_list_scroll_clear() { let mut s = PageList::init(80, 24, None).unwrap(); for y in [0, 1] { *s.get_cell_mut(active(0, y)).unwrap() = Cell::init('A' as u32); } s.scroll_clear().unwrap(); assert_viewport_point(&s, 2); 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(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, '😀' as u32, Wide::Wide); set_raw(&mut s, 1, 0, 0, Wide::SpacerTail); s.resize(Resize { cols: Some(1), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 1); assert_eq!(s.total_rows, 1); assert_eq!(s.first, s.last); assert_cell(&s, 0, 0, 0, Wide::Narrow); } #[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); } fn grow_rows(s: &mut PageList, count: usize) { for _ in 0..count { s.grow().unwrap(); } } fn screen_point(x: u16, y: usize) -> Point { Point::Screen(Coordinate { x, y: y as u32 }) } fn active_point(x: u16, y: usize) -> Point { Point::Active(Coordinate { x, y: y as u32 }) } fn mark_prompt(s: &mut PageList, y: usize, prompt: SemanticPrompt) { let p = s.pin(screen_point(0, y)).unwrap(); s.node_mut(p.node).unwrap().data.rows[usize::from(p.y)].set_semantic_prompt(prompt); } fn assert_scrollbar(s: &PageList, offset: usize) { assert_eq!( s.scrollbar(), Scrollbar { total: s.total_rows, offset, len: usize::from(s.rows) } ); } fn assert_viewport_point(s: &PageList, y: usize) { assert_eq!( s.point_from_pin( Tag::Screen, s.pin(Point::Viewport(Coordinate::default())).unwrap() ), Some(screen_point(0, y)) ); } #[test] fn page_list_grow_allows_exceeding_max_size_for_active_area() { let cap = initial_capacity(5); let mut s = PageList::init(5, cap.rows, Some(0)).unwrap(); assert_eq!(usize::from(s.rows), s.total_rows); s.grow().unwrap(); let start_pages = s.total_pages(); assert!(start_pages >= 2); for node in s.nodes.iter_mut().flatten() { node.data.size.rows = 1; node.data.capacity.rows = 1; } s.total_rows = start_pages; s.grow().unwrap(); assert_eq!(start_pages + 1, s.total_pages()); } #[test] fn page_list_grow_prune_required_with_a_single_page() { let mut s = PageList::init(80, 24, None).unwrap(); while Page::layout(s.node(s.first.unwrap()).unwrap().data.capacity).total_size <= std_size() { s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::GraphemeBytes)) .unwrap(); } assert!(s.first.is_some()); assert_eq!(s.first, s.last); let page = &s.node(s.first.unwrap()).unwrap().data; let rem = page.capacity.rows - page.size.rows; for _ in 0..rem { assert!(s.grow().unwrap().is_none()); } let new = s.grow().unwrap(); assert!(new.is_some()); assert_ne!(new, s.first); assert_scrollbar(&s, s.total_rows - usize::from(s.rows)); } #[test] fn page_list_scroll_delta_row_back_without_space_preserves_active() { let mut s = PageList::init(80, 24, None).unwrap(); s.scroll(Scroll::DeltaRow(-1)); assert_viewport_point(&s, 0); assert_eq!(s.viewport, Viewport::Active); assert_scrollbar(&s, s.total_rows - usize::from(s.rows)); } #[test] fn page_list_jump_zero_prompts() { let mut s = PageList::init(5, 3, None).unwrap(); grow_rows(&mut s, 3); assert_eq!(s.first, s.last); mark_prompt(&mut s, 1, SemanticPrompt::Prompt); mark_prompt(&mut s, 5, SemanticPrompt::Prompt); s.scroll(Scroll::DeltaPrompt(0)); assert_eq!(s.viewport, Viewport::Active); assert_scrollbar(&s, s.total_rows - usize::from(s.rows)); } #[test] fn screen_jump_back_one_prompt() { let mut s = PageList::init(5, 3, None).unwrap(); grow_rows(&mut s, 3); assert_eq!(s.first, s.last); mark_prompt(&mut s, 1, SemanticPrompt::Prompt); mark_prompt(&mut s, 5, SemanticPrompt::Prompt); for _ in 0..2 { s.scroll(Scroll::DeltaPrompt(-1)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, 1); assert_scrollbar(&s, 1); } for _ in 0..2 { s.scroll(Scroll::DeltaPrompt(1)); assert_eq!(s.viewport, Viewport::Active); assert_scrollbar(&s, s.total_rows - usize::from(s.rows)); } } #[test] fn screen_jump_forward_prompt_skips_multiline_continuation() { let mut s = PageList::init(5, 3, None).unwrap(); grow_rows(&mut s, 7); mark_prompt(&mut s, 1, SemanticPrompt::Prompt); mark_prompt(&mut s, 2, SemanticPrompt::PromptContinuation); mark_prompt(&mut s, 3, SemanticPrompt::PromptContinuation); mark_prompt(&mut s, 6, SemanticPrompt::Prompt); for start in [1, 2] { s.scroll(Scroll::Row(start)); s.scroll(Scroll::DeltaPrompt(1)); assert_eq!(s.viewport, Viewport::Pin); assert_viewport_point(&s, 6); } } fn cache_erase_fixture(pages: usize) -> (PageList, usize) { let mut s = PageList::init(80, 24, None).unwrap(); let capacity = usize::from(s.node(s.last.unwrap()).unwrap().data.capacity.rows); grow_rows(&mut s, capacity * pages); (s, capacity) } fn cache_pin(s: &mut PageList, y: usize) { s.scroll(Scroll::Pin(s.pin(screen_point(0, y)).unwrap())); assert_eq!(s.viewport, Viewport::Pin); assert_scrollbar(s, y); } #[test] fn page_list_erase_rows_invalidates_viewport_offset_cache() { let (mut s, capacity) = cache_erase_fixture(3); cache_pin(&mut s, capacity); let erased = capacity / 2; s.erase_rows( Point::History(Coordinate::default()), Some(Point::History(Coordinate { x: 0, y: (erased - 1) as u32, })), ) .unwrap(); assert_scrollbar(&s, capacity - erased); } #[test] fn page_list_erase_row_invalidates_viewport_offset_cache() { let (mut s, capacity) = cache_erase_fixture(3); cache_pin(&mut s, capacity); s.erase_row(Point::History(Coordinate::default())).unwrap(); assert_scrollbar(&s, capacity - 1); } #[test] fn page_list_erase_row_bounded_invalidates_viewport_offset_cache() { let (mut s, _) = cache_erase_fixture(3); cache_pin(&mut s, 4); s.erase_row_bounded(Point::History(Coordinate::default()), 10) .unwrap(); assert_scrollbar(&s, 3); } #[test] fn page_list_erase_row_bounded_multi_page_invalidates_viewport_offset_cache() { let (mut s, capacity) = cache_erase_fixture(3); cache_pin(&mut s, capacity + 1); s.erase_row_bounded(Point::History(Coordinate::default()), capacity + 10) .unwrap(); assert_scrollbar(&s, capacity); } #[test] fn page_list_erase_row_bounded_full_page_shift_invalidates_viewport_offset_cache() { let (mut s, capacity) = cache_erase_fixture(4); cache_pin(&mut s, 5); s.erase_row_bounded(Point::History(Coordinate::default()), capacity * 2 + 10) .unwrap(); assert_scrollbar(&s, 4); } #[test] fn page_list_erase_row_bounded_exhausts_pages_invalidates_viewport_offset_cache() { let (mut s, capacity) = cache_erase_fixture(3); let total_rows_before = s.total_rows; assert!(total_rows_before > usize::from(s.rows)); cache_pin(&mut s, capacity * 2 + 10); s.erase_row_bounded(Point::History(Coordinate::default()), total_rows_before * 2) .unwrap(); assert_scrollbar(&s, capacity * 2 + 9); } #[test] fn page_list_prompt_iterator_right_down_continuation_at_start() { let mut s = PageList::init(2, 20, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 0, SemanticPrompt::PromptContinuation); mark_prompt(&mut s, 1, SemanticPrompt::PromptContinuation); mark_prompt(&mut s, 5, SemanticPrompt::Prompt); let mut it = s.prompt_iterator(Direction::RightDown, screen_point(0, 0), None); for y in [0, 5] { assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(0, y)) ); } assert!(it.next().is_none()); } #[test] fn page_list_prompt_iterator_right_down_with_prompt_before_continuation() { let mut s = PageList::init(2, 20, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 2, SemanticPrompt::Prompt); mark_prompt(&mut s, 3, SemanticPrompt::PromptContinuation); mark_prompt(&mut s, 4, SemanticPrompt::PromptContinuation); let mut it = s.prompt_iterator(Direction::RightDown, screen_point(0, 3), None); assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(0, 3)) ); assert!(it.next().is_none()); } #[test] fn page_list_prompt_iterator_right_down_limit_inclusive() { let mut s = PageList::init(2, 20, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 5, SemanticPrompt::Prompt); mark_prompt(&mut s, 10, SemanticPrompt::Prompt); let mut it = s.prompt_iterator( Direction::RightDown, screen_point(0, 0), Some(screen_point(0, 5)), ); assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(0, 5)) ); assert!(it.next().is_none()); } #[test] fn page_list_prompt_iterator_left_up_limit_inclusive() { let mut s = PageList::init(2, 20, Some(0)).unwrap(); assert_eq!(s.first, s.last); mark_prompt(&mut s, 5, SemanticPrompt::Prompt); mark_prompt(&mut s, 10, SemanticPrompt::Prompt); let mut it = s.prompt_iterator( Direction::LeftUp, screen_point(0, 10), Some(screen_point(0, 15)), ); assert_eq!( s.point_from_pin(Tag::Screen, it.next().unwrap()), Some(screen_point(0, 10)) ); assert!(it.next().is_none()); } #[test] fn page_list_erase_resets_viewport_to_active_if_moves_within_active() { let (mut s, _) = cache_erase_fixture(5); s.scroll(Scroll::DeltaRow(-(s.total_rows as isize))); assert_eq!(s.viewport, Viewport::Top); s.erase_rows(Point::History(Coordinate::default()), None) .unwrap(); assert_eq!(s.viewport, Viewport::Active); } #[test] fn page_list_erase_resets_viewport_if_inside_erased_page_but_not_active() { let (mut s, _) = cache_erase_fixture(5); s.scroll(Scroll::DeltaRow(-(s.total_rows as isize))); assert_eq!(s.viewport, Viewport::Top); s.erase_rows( Point::History(Coordinate::default()), Some(Point::History(Coordinate { x: 0, y: 2 })), ) .unwrap(); assert_eq!(s.viewport, Viewport::Top); } #[test] fn page_list_erase_resets_viewport_to_active_if_top_is_inside_active() { let (mut s, _) = cache_erase_fixture(5); s.scroll(Scroll::Top); s.erase_rows(Point::History(Coordinate::default()), None) .unwrap(); assert_eq!(s.viewport, Viewport::Active); } fn shrink_rows_cursor_fixture(y: usize) -> (PageList, TrackedPin) { let mut s = PageList::init(10, 10, Some(0)).unwrap(); assert_eq!(s.total_rows, 10); assert_eq!(s.first, s.last); for row in 0..10 { *s.get_cell_mut(active_point(0, row)).unwrap() = Cell::init(row as u32); } let p = s.track_pin(s.pin(active_point(0, y)).unwrap()).unwrap(); let cursor = s .point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()) .unwrap(); assert_eq!(s.get_cell(cursor).unwrap().codepoint(), y as u32); s.resize(Resize { rows: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.rows, 5); assert_eq!(s.total_rows, 10); (s, p) } #[test] fn page_list_resize_no_reflow_less_rows_cursor_on_bottom() { let (s, p) = shrink_rows_cursor_fixture(9); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(0, 4)) ); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 5)) ); } #[test] fn page_list_resize_no_reflow_less_rows_cursor_in_scrollback() { let (s, p) = shrink_rows_cursor_fixture(2); assert!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()) .is_none() ); assert_eq!( s.point_from_pin(Tag::Screen, s.tracked_pin(p).unwrap()), Some(screen_point(0, 2)) ); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 5)) ); } #[test] fn page_list_resize_no_reflow_more_rows_contains_viewport() { let mut s = PageList::init(5, 5, Some(1)).unwrap(); assert_eq!(s.first, s.last); s.grow().unwrap(); assert_eq!(s.rows, 5); assert_eq!(s.total_rows, 6); s.scroll(Scroll::DeltaRow(-1)); assert_eq!(s.viewport, Viewport::Top); s.resize(Resize { rows: Some(7), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.rows, 7); assert_eq!(s.total_rows, 7); assert_eq!(s.viewport, Viewport::Top); } fn set_raw(s: &mut PageList, x: u16, y: usize, cp: u32, wide: Wide) { let cell = s.get_cell_mut(screen_point(x, y)).unwrap(); *cell = Cell::init(cp); cell.set_wide(wide); } fn set_wrap(s: &mut PageList, y: usize, wrap: bool, continuation: bool) { let p = s.pin(screen_point(0, y)).unwrap(); let row = &mut s.node_mut(p.node).unwrap().data.rows[usize::from(p.y)]; row.set_wrap(wrap); row.set_wrap_continuation(continuation); } fn assert_cell(s: &PageList, x: u16, y: usize, cp: u32, wide: Wide) { let c = s.get_cell(screen_point(x, y)).unwrap(); assert_eq!(c.codepoint(), cp); assert_eq!(c.wide(), wide); } fn spacer_fixture(rows: u16) -> PageList { let mut s = PageList::init(2, rows, Some(0)).unwrap(); assert_eq!(s.first, s.last); set_raw(&mut s, 0, 0, 'x' as u32, Wide::Narrow); set_raw(&mut s, 1, 0, 0, Wide::SpacerHead); set_wrap(&mut s, 0, true, false); set_raw(&mut s, 0, 1, '😀' as u32, Wide::Wide); set_raw(&mut s, 1, 1, 0, Wide::SpacerTail); s } #[test] fn page_list_resize_no_reflow_more_cols_with_spacer_head() { let mut s = spacer_fixture(3); s.resize(Resize { cols: Some(3), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 3); assert_eq!(s.total_rows, 3); assert_eq!(s.first, s.last); assert_cell(&s, 0, 0, 'x' as u32, Wide::Narrow); assert_cell(&s, 1, 0, 0, Wide::Narrow); assert_cell(&s, 2, 0, 0, Wide::Narrow); } #[test] fn page_list_resize_no_reflow_grow_cols_fast_path_with_spacer_head() { let mut s = PageList::init(10, 3, Some(0)).unwrap(); s.resize(Resize { cols: Some(5), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); set_raw(&mut s, 0, 0, 'x' as u32, Wide::Narrow); for y in 0..2 { set_raw(&mut s, 4, y, 0, Wide::SpacerHead); set_wrap(&mut s, y, true, false); } s.resize(Resize { cols: Some(10), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 10); for y in 0..2 { let p = s.pin(screen_point(4, y)).unwrap(); let (row, cell) = p.row_and_cell(&s).unwrap(); assert_eq!(cell.wide(), Wide::Narrow); assert!(!row.wrap()); } } fn assert_pages_full_except_last(s: &PageList) { let mut node = s.first; while let Some(id) = node { if Some(id) == s.last { break; } let n = s.node(id).unwrap(); assert_eq!(n.data.size.rows, n.data.capacity.rows); node = n.next; } } #[test] fn page_list_resize_no_reflow_more_cols_forces_less_rows_per_page() { assert!(initial_capacity(5).rows >= 150); let mut s = PageList::init(5, 150, Some(0)).unwrap(); let mut cols = 50; while initial_capacity(cols).rows >= 150 { cols += 50; } s.resize(Resize { cols: Some(cols), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, cols); assert_eq!(s.total_rows, 150); assert_pages_full_except_last(&s); let page = &s.node(s.first.unwrap()).unwrap().data; assert_eq!(page.size.rows, page.capacity.rows); let previous_rows = page.size.rows; cols = page.size.cols + 50; while initial_capacity(cols).rows >= previous_rows { cols += 50; } s.resize(Resize { cols: Some(cols), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.cols, cols); assert_eq!(s.total_rows, 150); assert_pages_full_except_last(&s); } #[test] fn page_list_resize_more_rows_and_cols_doesnt_fit_in_single_std_page() { let mut s = PageList::init(10, 10, Some(0)).unwrap(); assert!(initial_capacity(600).rows < 600); s.resize(Resize { cols: Some(600), rows: Some(600), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 600); assert_eq!(s.rows, 600); assert_eq!(s.total_rows, 600); } #[test] fn page_list_resize_no_reflow_more_cols_forces_smaller_cap() { let cap = initial_capacity(100); let cap2 = initial_capacity(500); assert_eq!(cap2.cols, 500); assert!(cap2.rows < cap.rows); let mut s = PageList::init(cap.cols, cap.rows, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..usize::from(s.rows) { for x in 0..s.cols { set_raw(&mut s, x, y, 'A' as u32, Wide::Narrow); } } let rows = s.total_rows; s.resize(Resize { cols: Some(cap2.cols), reflow: false, ..Resize::default() }) .unwrap(); assert_eq!(s.total_rows, rows); for p in s.row_iterator(Direction::RightDown, screen_point(0, 0), None) { let cells = p.cells(&s, CellSubset::All).unwrap(); assert_eq!(cells.len(), usize::from(cap2.cols)); assert_eq!(cells[0].codepoint(), 'A' as u32); } } #[test] fn page_list_resize_no_reflow_more_rows_adds_blank_rows_if_cursor_at_bottom() { let mut s = PageList::init(5, 3, None).unwrap(); grow_rows(&mut s, 2); assert_eq!(s.first, s.last); for y in 0..s.total_rows { set_raw(&mut s, 0, y, y as u32, Wide::Narrow); } assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 2)) ); let p = s .track_pin(s.pin(active_point(0, usize::from(s.rows - 2))).unwrap()) .unwrap(); let original_cursor = s .point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()) .unwrap(); assert_eq!(s.get_cell(original_cursor).unwrap().codepoint(), 3); s.resize(Resize { rows: Some(10), reflow: false, cursor: Some(Cursor { x: 0, y: s.rows - 2, }), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 5); assert_eq!(s.rows, 10); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(original_cursor) ); assert_eq!(s.total_rows, 12); assert_eq!( s.point_from_pin(Tag::Screen, s.get_top_left(Tag::Active).unwrap()), Some(screen_point(0, 2)) ); for y in 0..3 { assert_eq!( s.get_cell(active_point(0, y)).unwrap().codepoint(), (y + 2) as u32 ); } } #[test] fn page_list_resize_reflow_invalidates_viewport_offset_cache() { let mut s = PageList::init(2, 4, None).unwrap(); grow_rows(&mut s, 20); for y in 0..usize::from(s.rows) { set_wrap(&mut s, y, y % 2 == 0, y % 2 != 0); for x in 0..s.cols { set_raw(&mut s, x, y, 'A' as u32, Wide::Narrow); } } cache_pin(&mut s, 10); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_scrollbar(&s, 5); } #[test] fn page_list_resize_reflow_more_cols_creates_multiple_pages() { let mut cols = 100; while initial_capacity(cols).rows >= 100 { cols += 100; } let cap = initial_capacity(cols); let mut s = PageList::init(cap.cols, cap.rows, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..usize::from(s.rows) { set_wrap(&mut s, y, y % 2 == 0, y % 2 != 0); set_raw(&mut s, 0, y, 'A' as u32, Wide::Narrow); } let newcap = cap .adjust(Adjustment { cols: Some(cap.cols + 100), }) .unwrap(); assert!(newcap.rows < cap.rows); s.resize(Resize { cols: Some(newcap.cols), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, newcap.cols); assert_eq!(s.total_rows, usize::from(cap.rows)); for n in s.nodes.iter().flatten() { assert_eq!(n.data.capacity.cols, newcap.cols); assert_eq!(n.data.capacity.rows, newcap.rows); } assert!(s.total_pages() > 1); } fn two_page_fixture(cols: u16, extra: usize, max: Option) -> PageList { let mut s = PageList::init(cols, 10, max).unwrap(); assert_eq!(s.total_pages(), 1); let page = &s.node(s.first.unwrap()).unwrap().data; let rem = usize::from(page.capacity.rows - page.size.rows); grow_rows(&mut s, rem); assert_eq!(s.total_pages(), 1); grow_rows(&mut s, extra); assert_eq!(s.total_pages(), 2); s } fn wrap_page_boundary(s: &mut PageList) { let y = usize::from(s.node(s.first.unwrap()).unwrap().data.size.rows - 1); set_wrap(s, y, true, false); set_wrap(s, y + 1, false, true); for row in [y, y + 1] { for x in 0..s.cols { set_raw(s, x, row, u32::from(x), Wide::Narrow); } } } fn assert_active_row(s: &PageList, y: usize, wrap: bool, continuation: bool, cps: &[u32]) { let p = s.pin(active_point(0, y)).unwrap(); let (row, _) = p.row_and_cell(s).unwrap(); assert_eq!(row.wrap(), wrap); assert_eq!(row.wrap_continuation(), continuation); let cells = p.cells(s, CellSubset::All).unwrap(); for (cell, cp) in cells.iter().zip(cps) { assert_eq!(cell.codepoint(), *cp); } } #[test] fn page_list_resize_reflow_more_cols_wrap_across_page_boundary() { let mut s = two_page_fixture(2, 1, Some(0)); wrap_page_boundary(&mut s); let end_rows = s.total_rows - 1; s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, end_rows); assert_active_row(&s, 8, false, false, &[0; 4]); for c in s .pin(active_point(0, 8)) .unwrap() .cells(&s, CellSubset::All) .unwrap() { assert!(!c.has_text()); } assert_active_row(&s, 9, false, false, &[0, 1, 0, 1]); } #[test] fn page_list_resize_reflow_more_cols_wrap_across_page_boundary_cursor_in_second_page() { let mut s = two_page_fixture(2, 1, Some(0)); wrap_page_boundary(&mut s); let p = s.track_pin(s.pin(active_point(1, 9)).unwrap()).unwrap(); assert_eq!(Some(s.tracked_pin(p).unwrap().node), s.last); let end_rows = s.total_rows - 1; s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, end_rows); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(3, 9)) ); assert_active_row(&s, 9, false, false, &[0, 1, 0, 1]); } #[test] fn page_list_resize_reflow_less_cols_wrap_across_page_boundary_cursor_in_second_page() { let mut s = two_page_fixture(5, 5, None); wrap_page_boundary(&mut s); let p = s.track_pin(s.pin(active_point(2, 5)).unwrap()).unwrap(); assert_eq!(Some(s.tracked_pin(p).unwrap().node), s.last); assert_eq!(s.tracked_pin(p).unwrap().y, 0); s.resize(Resize { cols: Some(4), cursor: Some(Cursor { x: 2, y: 5 }), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(3, 5)) ); for y in [3, 7] { assert_active_row(&s, y, false, false, &[0; 4]); for c in s .pin(active_point(0, y)) .unwrap() .cells(&s, CellSubset::All) .unwrap() { assert!(!c.has_text()); } } assert_active_row(&s, 4, true, false, &[0, 1, 2, 3]); assert_active_row(&s, 5, true, true, &[4, 0, 1, 2]); assert_active_row(&s, 6, false, true, &[3, 4]); } fn grow_cursor_fixture(wrapped_rows: usize, cursor_y: usize) -> (PageList, TrackedPin) { let mut s = PageList::init(2, 4, Some(0)).unwrap(); assert_eq!(s.first, s.last); for y in 0..wrapped_rows { set_wrap(&mut s, y, y + 1 < wrapped_rows, y != 0); for x in 0..s.cols { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s .track_pin(s.pin(active_point(1, cursor_y)).unwrap()) .unwrap(); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 4); (s, p) } #[test] fn page_list_resize_reflow_more_cols_cursor_in_wrapped_row() { let (s, p) = grow_cursor_fixture(2, 1); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(3, 0)) ); } #[test] fn page_list_resize_reflow_more_cols_cursor_in_not_wrapped_row() { let (s, p) = grow_cursor_fixture(2, 0); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(1, 0)) ); } #[test] fn page_list_resize_reflow_more_cols_cursor_in_wrapped_row_that_isnt_unwrapped() { let (s, p) = grow_cursor_fixture(3, 2); assert_eq!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()), Some(active_point(1, 1)) ); } #[test] fn page_list_resize_reflow_more_cols_unwrap_wide_spacer_head() { let mut s = spacer_fixture(2); set_wrap(&mut s, 1, false, true); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 2); assert_eq!(s.first, s.last); assert_cell(&s, 0, 0, 'x' as u32, Wide::Narrow); assert!( !s.pin(screen_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); assert_cell(&s, 1, 0, '😀' as u32, Wide::Wide); assert_cell(&s, 2, 0, 0, Wide::SpacerTail); } #[test] fn page_list_resize_reflow_more_cols_unwrap_wide_spacer_head_across_two_rows() { let mut s = PageList::init(2, 3, Some(0)).unwrap(); assert_eq!(s.first, s.last); for (x, y) in [(0, 0), (1, 0), (0, 1)] { set_raw(&mut s, x, y, 'x' as u32, Wide::Narrow); } set_raw(&mut s, 1, 1, 0, Wide::SpacerHead); set_raw(&mut s, 0, 2, '😀' as u32, Wide::Wide); set_raw(&mut s, 1, 2, 0, Wide::SpacerTail); for y in 0..3 { set_wrap(&mut s, y, y < 2, y > 0); } s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 3); assert_eq!(s.first, s.last); for x in 0..3 { assert_cell(&s, x, 0, 'x' as u32, Wide::Narrow); } assert!( s.pin(screen_point(0, 0)) .unwrap() .row_and_cell(&s) .unwrap() .0 .wrap() ); assert_cell(&s, 3, 0, 0, Wide::SpacerHead); assert_cell(&s, 0, 1, '😀' as u32, Wide::Wide); assert_cell(&s, 1, 1, 0, Wide::SpacerTail); } #[test] fn page_list_resize_reflow_less_cols_cursor_goes_to_scrollback() { let mut s = PageList::init(4, 2, None).unwrap(); assert_eq!(s.first, s.last); for y in 0..2 { for x in 0..4 { set_raw(&mut s, x, y, u32::from(x), Wide::Narrow); } } let p = s.track_pin(s.pin(active_point(2, 0)).unwrap()).unwrap(); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 4); assert!( s.point_from_pin(Tag::Active, s.tracked_pin(p).unwrap()) .is_none() ); } #[test] fn page_list_resize_reflow_exceeds_hyperlink_memory_forcing_capacity_increase() { use crate::hyperlink::{Hyperlink, HyperlinkId}; let mut s = two_page_fixture(2, 1, Some(0)); assert_ne!(s.first, s.last); assert_eq!(s.node(s.first.unwrap()).unwrap().next, s.last); for (index, node) in [s.first.unwrap(), s.last.unwrap()].into_iter().enumerate() { let page = &mut s.node_mut(node).unwrap().data; let (x, y) = if index == 0 { (page.size.cols - 1, page.size.rows - 1) } else { (0, 0) }; let id = page .insert_hyperlink(Hyperlink { id: HyperlinkId::Implicit(index as u32), uri: vec![b'a'; page::STRING_BYTES_DEFAULT as usize - 1], }) .unwrap(); let i = page.cell_index(usize::from(x), usize::from(y)).unwrap(); page.cells[i] = Cell::init('X' as u32); page.rows[usize::from(y)].set_wrap(index == 0); page.rows[usize::from(y)].set_wrap_continuation(index == 1); page.set_hyperlink(usize::from(x), usize::from(y), id) .unwrap(); assert_eq!( page.insert_hyperlink(Hyperlink { id: HyperlinkId::Implicit(index as u32 + 1), uri: b"AAAAAAAAAAAAAAAAAAAAAAAAAA".to_vec() }), Err(Error::StringsOutOfMemory) ); } s.resize(Resize { cols: Some(s.cols + 1), ..Resize::default() }) .unwrap(); s.assert_integrity(); } #[test] fn page_list_resize_reflow_exceeds_grapheme_memory_forcing_capacity_increase() { let mut s = two_page_fixture(2, 1, Some(0)); assert_ne!(s.first, s.last); assert_eq!(s.node(s.first.unwrap()).unwrap().next, s.last); for (index, node) in [s.first.unwrap(), s.last.unwrap()].into_iter().enumerate() { let page = &mut s.node_mut(node).unwrap().data; let (x, y) = if index == 0 { (page.size.cols - 1, page.size.rows - 1) } else { (0, 0) }; let i = page.cell_index(usize::from(x), usize::from(y)).unwrap(); page.cells[i] = Cell::init('X' as u32); page.rows[usize::from(y)].set_wrap(true); page.set_graphemes( usize::from(x), usize::from(y), &vec!['a' as u32; (page::GRAPHEME_BYTES_DEFAULT as usize - 1) / 4], ) .unwrap(); assert!(!page.can_allocate_grapheme(16 * 4)); } s.resize(Resize { cols: Some(s.cols + 1), ..Resize::default() }) .unwrap(); s.assert_integrity(); } #[test] fn page_list_resize_reflow_exceeds_style_memory_forcing_capacity_increase() { use ghostty_vt::color::Rgb; use crate::style::{Color, Style}; let mut s = two_page_fixture(page::STD_CAPACITY.styles - 1, 1, Some(0)); assert_ne!(s.first, s.last); assert_eq!(s.node(s.first.unwrap()).unwrap().next, s.last); for (index, node) in [s.first.unwrap(), s.last.unwrap()].into_iter().enumerate() { let page = &mut s.node_mut(node).unwrap().data; let y = if index == 0 { page.size.rows - 1 } else { 0 }; for x in 0..usize::from(page.size.cols) { let color = Color::Rgb(Rgb { r: x as u8, g: (x >> 8) as u8, b: (x >> 16) as u8, }); let style = if index == 0 { Style { bg_color: color, ..Style::default() } } else { Style { fg_color: color, ..Style::default() } }; let Ok(id) = page.styles.add(style) else { break; }; let i = page.cell_index(x, usize::from(y)).unwrap(); page.cells[i] = Cell::init('X' as u32); page.cells[i].set_style_id(id); page.rows[usize::from(y)].set_styled(true); page.rows[usize::from(y)].set_wrap(index == 0); page.rows[usize::from(y)].set_wrap_continuation(index == 1); } } s.resize(Resize { cols: Some(s.cols * 2), ..Resize::default() }) .unwrap(); s.assert_integrity(); } fn kitty_fixture(start: u16) -> PageList { let mut s = PageList::init(4, 2, Some(0)).unwrap(); assert_eq!(s.first, s.last); for x in start..s.cols - 1 { set_raw(&mut s, x, 0, 0x10eeee, Wide::Narrow); } s.node_mut(s.first.unwrap()).unwrap().data.rows[0].set_kitty_virtual_placeholder(true); s } #[test] fn page_list_resize_reflow_less_cols_copy_kitty_placeholder() { let mut s = kitty_fixture(0); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 2); for p in s.row_iterator(Direction::RightDown, active_point(0, 0), None) { for x in 0..s.cols - 1 { assert!( Pin { x, ..p } .row_and_cell(&s) .unwrap() .0 .kitty_virtual_placeholder() ); } } } #[test] fn page_list_resize_reflow_more_cols_clears_kitty_placeholder() { let mut s = kitty_fixture(0); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert_eq!(s.total_rows, 2); let mut it = s.row_iterator(Direction::RightDown, active_point(0, 0), None); assert!( it.next() .unwrap() .row_and_cell(&s) .unwrap() .0 .kitty_virtual_placeholder() ); assert!( !it.next() .unwrap() .row_and_cell(&s) .unwrap() .0 .kitty_virtual_placeholder() ); assert!(it.next().is_none()); } #[test] fn page_list_resize_reflow_wrap_moves_kitty_placeholder() { let mut s = kitty_fixture(2); s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); assert_eq!(s.total_rows, 2); let mut it = s.row_iterator(Direction::RightDown, active_point(0, 0), None); assert!( !it.next() .unwrap() .row_and_cell(&s) .unwrap() .0 .kitty_virtual_placeholder() ); assert!( it.next() .unwrap() .row_and_cell(&s) .unwrap() .0 .kitty_virtual_placeholder() ); assert!(it.next().is_none()); } #[test] fn page_list_resize_reflow_grapheme_map_capacity_exceeded() { let mut s = PageList::init(4, 10, Some(0)).unwrap(); assert_eq!(s.total_pages(), 1); let page = &s.node(s.first.unwrap()).unwrap().data; let capacity = page.grapheme_capacity(); let per_page = capacity / 2 + capacity / 4; let rem = usize::from(page.capacity.rows - page.size.rows); grow_rows(&mut s, rem); assert_eq!(s.total_pages(), 1); grow_rows(&mut s, per_page); assert_eq!(s.total_pages(), 2); assert_ne!(s.first, s.last); assert_eq!(s.node(s.first.unwrap()).unwrap().next, s.last); for (index, node) in [s.first.unwrap(), s.last.unwrap()].into_iter().enumerate() { let page = &mut s.node_mut(node).unwrap().data; let start = if index == 0 { usize::from(page.size.rows) - per_page } else { 0 }; let count = per_page.min(usize::from(page.size.rows)); for y in start..start + count { let i = page.cell_index(0, y).unwrap(); page.cells[i] = Cell::init(if index == 0 { 'A' } else { 'B' } as u32); page.append_grapheme(0, y, 0x0301 + index as u32).unwrap(); } } s.resize(Resize { cols: Some(2), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 2); s.assert_integrity(); } #[test] fn page_list_resize_grow_cols_with_unwrap_fixes_viewport_pin() { let mut s = PageList::init(2, 10, None).unwrap(); grow_rows(&mut s, 30); assert_eq!(s.total_rows, 40); for y in 0..40 { set_wrap(&mut s, y, y % 2 == 0, y % 2 != 0); for x in 0..2 { set_raw(&mut s, x, y, 'A' as u32, Wide::Narrow); } } s.scroll(Scroll::Pin(s.pin(screen_point(0, 28)).unwrap())); assert_eq!(s.viewport, Viewport::Pin); assert!(s.get_bottom_right(Tag::Viewport).is_some()); s.resize(Resize { cols: Some(4), ..Resize::default() }) .unwrap(); assert_eq!(s.cols, 4); assert!(s.total_rows < 40); assert!(s.get_bottom_right(Tag::Viewport).is_some()); } fn oversize_first(s: &mut PageList) { while Page::layout(s.node(s.first.unwrap()).unwrap().data.capacity).total_size <= std_size() { s.increase_capacity(s.first.unwrap(), Some(IncreaseCapacity::GraphemeBytes)) .unwrap(); } assert!( Page::layout(s.node(s.first.unwrap()).unwrap().data.capacity).total_size > std_size() ); } #[test] fn page_list_grow_reuses_non_standard_page_without_leak() { let mut s = PageList::init(80, 24, Some(3 * std_size())).unwrap(); oversize_first(&mut s); let first = s.first.unwrap(); let page = &s.node(first).unwrap().data; let rem = usize::from(page.capacity.rows - page.size.rows); grow_rows(&mut s, rem); s.grow().unwrap(); assert_ne!(s.first, s.last); while s.page_size + std_size() <= s.max_size() || s.node(s.last.unwrap()).unwrap().data.size.rows < s.node(s.last.unwrap()).unwrap().data.capacity.rows { s.grow().unwrap(); } assert!( Page::layout(s.node(s.first.unwrap()).unwrap().data.capacity).total_size > std_size() ); assert!(s.total_rows >= usize::from(s.rows)); assert_eq!( s.node(s.last.unwrap()).unwrap().data.size.rows, s.node(s.last.unwrap()).unwrap().data.capacity.rows ); let first_page = s.first.unwrap(); let first_allocation = first_page; let tracked = s .track_pin(Pin { node: first_page, x: 0, y: 0, garbage: false, }) .unwrap(); s.grow().unwrap(); assert_ne!(s.first, Some(first_page)); // Rust may reuse the freed Vec address. Node generations identify the // allocation, and oversized pages must never enter the standard pool. assert_ne!(s.last, Some(first_allocation)); assert!( s.page_pool .iter() .all(|page| Page::layout(page.capacity).total_size <= std_size()) ); let p = s.tracked_pin(tracked).unwrap(); assert_eq!(Some(p.node), s.first); assert_eq!(p.x, 0); assert_eq!(p.y, 0); assert!(p.garbage); s.assert_integrity(); } #[test] fn page_list_grow_non_standard_page_prune_protection() { let mut s = PageList::init(80, 600, Some(std_size())).unwrap(); oversize_first(&mut s); let first = s.first.unwrap(); let first_cap = s.node(first).unwrap().data.capacity.rows; while s.node(first).unwrap().data.size.rows < first_cap { s.grow().unwrap(); } while s.first == s.last { s.grow().unwrap(); } assert_ne!(s.first, s.last); let last = s.last.unwrap(); let second_cap = s.node(last).unwrap().data.capacity.rows; while s.node(last).unwrap().data.size.rows < second_cap { s.grow().unwrap(); } let would_remain = s.total_rows - usize::from(first_cap) + 1; assert!(would_remain < usize::from(s.rows)); assert_ne!(s.first, s.last); assert!(s.page_size + std_size() > s.max_size()); assert!(s.total_rows >= usize::from(s.rows)); assert_eq!(s.node(last).unwrap().data.size.rows, second_cap); s.grow().unwrap(); assert!(s.total_rows >= usize::from(s.rows)); } #[test] fn page_list_resize_no_reflow_more_cols_remaps_pins_in_backfill_path() { let cap = initial_capacity(5); let mut s = PageList::init(5, cap.rows, None).unwrap(); while s.first == s.last { s.grow().unwrap(); } let first = s.first.unwrap(); let second = s.last.unwrap(); assert_ne!(first, second); s.erase_rows( Point::History(Coordinate::default()), Some(Point::History(Coordinate::default())), ) .unwrap(); assert!(s.node(first).unwrap().data.size.rows < s.node(first).unwrap().data.capacity.rows); let adjusted = s .node(second) .unwrap() .data .capacity .adjust(Adjustment { cols: Some(6) }) .unwrap(); assert!(s.node(second).unwrap().data.capacity.cols < adjusted.cols); let tracked = s .track_pin(Pin { node: second, x: 0, y: 0, garbage: false, }) .unwrap(); s.node_mut(second).unwrap().data.get_cells_mut(0).unwrap()[0] = Cell::init('X' as u32); s.resize(Resize { cols: Some(6), reflow: false, ..Resize::default() }) .unwrap(); let p = s.tracked_pin(tracked).unwrap(); let mut node = s.first; let mut found = false; while let Some(id) = node { if id == p.node { found = true; break; } node = s.node(id).unwrap().next; } assert!(found); assert!(p.y < s.node(p.node).unwrap().data.size.rows); let cell = p.row_and_cell(&s).unwrap().1; assert_eq!(cell.content_tag(), ContentTag::Codepoint); assert_eq!(cell.codepoint(), 'X' as u32); } #[test] fn active_erasure_preserves_history_and_regrows_removed_rows() { let mut s = PageList::init(8, 4, None).unwrap(); grow(&mut s, 8); for y in 0..s.total_rows { set_raw(&mut s, 0, y, 65 + y as u32, Wide::Narrow); } s.scroll(Scroll::Row(3)); assert_eq!(s.viewport_row_offset(), 3); let total = s.total_rows; s.erase_rows(active(0, 0), Some(active(0, 1))).unwrap(); assert_eq!(s.total_rows, total); assert_eq!(s.viewport, Viewport::Pin); assert_eq!(s.viewport_row_offset(), 3); for y in 0..8 { assert_eq!(s.get_cell(screen(0, y)).unwrap().codepoint(), 65 + y); } assert_eq!(s.get_cell(active(0, 0)).unwrap().codepoint(), 75); assert_eq!(s.get_cell(active(0, 1)).unwrap().codepoint(), 76); for y in 2..4 { assert!(s.get_cell(active(0, y)).unwrap().is_empty()); } finish(&s); } #[test] fn pruning_does_not_populate_invalid_viewport_offset_cache() { let mut s = PageList::init(80, 24, Some(1)).unwrap(); fill_pages(&mut s, 2); let target = s.pin(screen(0, 5)).unwrap(); s.scroll(Scroll::Pin(target)); assert_eq!( s.viewport_pin_row_offset.load(Ordering::Relaxed), usize::MAX ); s.grow().unwrap(); assert_eq!( s.viewport_pin_row_offset.load(Ordering::Relaxed), usize::MAX ); assert_eq!(s.viewport, Viewport::Pin); assert_eq!(s.viewport_row_offset(), 0); finish(&s); } #[test] fn pin_iterators_use_inclusive_rows_across_pages() { let mut list = PageList::init(4, 4, None).unwrap(); list.split(list.pin(active_point(0, 2)).unwrap()).unwrap(); let start = list.pin(active_point(2, 1)).unwrap(); let limit = list.pin(active_point(1, 2)).unwrap(); let cells: Vec<_> = start .cell_iterator(&list, Direction::RightDown, Some(limit)) .collect(); assert_eq!(cells.len(), 6); assert_eq!(cells[0], start); assert_eq!(cells[5].x, 3); let reverse: Vec<_> = limit .cell_iterator(&list, Direction::LeftUp, Some(start)) .collect(); assert_eq!(reverse.len(), 6); assert_eq!(reverse[0], limit); assert_eq!(reverse[5].x, 0); for (start, limit, direction) in [ (start, limit, Direction::RightDown), (limit, start, Direction::LeftUp), ] { assert_eq!(start.row_iterator(&list, direction, Some(limit)).count(), 2); let pages: Vec<_> = start.page_iterator(&list, direction, Some(limit)).collect(); assert_eq!(pages.len(), 2); assert!(pages.iter().all(|c| c.end - c.start == 1)); } } #[test] fn row_copy_between_pages_preserves_managed_values_and_metadata() { let mut list = PageList::init(4, 4, None).unwrap(); list.split(list.pin(active_point(0, 2)).unwrap()).unwrap(); let src = list.pin(active_point(0, 0)).unwrap(); let dst = list.pin(active_point(0, 2)).unwrap(); let page = &mut list.node_mut(src.node).unwrap().data; page.cells[1] = Cell::init('a' as u32); page.append_grapheme(1, 0, 0x301).unwrap(); let id = page .insert_hyperlink(crate::hyperlink::Hyperlink { uri: b"https://example.com".to_vec(), id: crate::hyperlink::HyperlinkId::Implicit(1), }) .unwrap(); page.set_hyperlink(1, 0, id).unwrap(); page.rows[0].set_kitty_virtual_placeholder(true); page.rows[0].set_dirty(false); let page = &mut list.node_mut(dst.node).unwrap().data; page.cells[0] = Cell::init('z' as u32); page.rows[0].set_wrap(true); page.rows[0].set_dirty(true); list.clone_partial_row_between(src, dst, 1, 2).unwrap(); let page = &list.node(dst.node).unwrap().data; assert_eq!(page.get_cells(0).unwrap()[0].codepoint(), 'z' as u32); assert!(page.rows[0].wrap()); assert!(page.rows[0].dirty()); assert!(page.rows[0].kitty_virtual_placeholder()); assert_eq!( page.lookup_grapheme(page.cell_index(1, 0).unwrap()), Some(&[0x301][..]) ); assert!( page.lookup_hyperlink(page.cell_index(1, 0).unwrap()) .is_some() ); list.clone_row_between(src, dst).unwrap(); let page = &list.node(dst.node).unwrap().data; assert_eq!(page.get_cells(0).unwrap()[0].codepoint(), 0); assert!(!page.rows[0].wrap()); assert!(!page.rows[0].dirty()); list.assert_integrity(); } #[test] fn erase_empty_history_and_top_viewport() { let mut list = PageList::init(4, 4, None).unwrap(); list.erase_rows(Point::History(Coordinate::default()), None) .unwrap(); assert_eq!(list.total_rows, 4); list.grow().unwrap(); list.scroll(Scroll::Top); list.erase_rows(Point::History(Coordinate::default()), None) .unwrap(); assert_eq!(list.total_rows, 4); assert_eq!(list.viewport, Viewport::Active); list.assert_integrity(); } }