// fold_view.rs --- The visible-line map (Arc 6, Stage 2). //! The source-line ↔ display-row projection that folding introduces. //! //! Before folding, every grid consumer assumed `display_row = //! source_line − view_top` — an identity map baked into the text walk, //! the gutter, every overlay, the caret, both selection painters, the //! mode-line indicator, and the click/scroll/motion inverses. //! [`DisplayCoord`](crate::view::DisplayCoord) anticipated a //! non-identity map "once virtual lines, wrapping, and inline //! expansions appear"; **folding is the first**. //! //! This module is that map — **one derivation/query primitive** //! (`docs/folding-stage2-framing.md` Q#FD12), derived from //! [`crate::fold::FoldRegistry::folds`] plus the buffer's line offsets //! and **never stored**: the byte-range store in [`crate::fold`] stays //! the single source of truth. Instances are short-lived and built //! **per rendered window** and **per command/event operation**, never //! once per frame — a frame paints several windows that may show //! different buffers, so a per-frame singleton would leak one pane's //! folds into another's (framing round-2 F2). //! //! # Hidden components, not folds //! //! The unit here is not a fold. [`crate::fold::FoldStore::insert`] //! accepts any normalized range, so folds may nest, share a head line, //! or **cross**: with fold `A` hiding lines 1–3 and fold `B` headed on //! line 2 hiding lines 3–5, a point on line 5 is directly inside only //! `B` — yet `B`'s own head is hidden by `A`, so projecting to `B`'s //! `range.start` would land on another *hidden* position (round-3 F2). //! //! The derivation therefore unions overlapping **or adjacent** hidden //! line intervals into sorted, non-overlapping **hidden components**. //! Adjacent intervals merge because the later fold's head is hidden by //! the earlier one, so it can never render. Each component keeps the one //! visible line immediately before it (`head_line`) and that line's exact //! end-of-content byte (`head_position` — the fold `range.start` Stage 1 //! already moves point to). Resolving through the component is //! equivalent to repeatedly projecting a hidden fold head until it is //! visible, and so covers nesting, shared heads, and crossing overlap //! alike. use pmacs_protocol::ByteRange; use crate::fold::FoldRegistry; use crate::rope::Position; use crate::window::Window; /// The visible-line map for one **window's** buffer, or `None` when that /// buffer has no folds. /// /// The single construction rule shared by the render path and /// `EditorCore` (Q#FD12): keyed on *this* window's `buffer_id` and its /// own [`TextView`](crate::text_view::TextView) line offsets — never the /// active buffer's — so a split showing two buffers gets two independent /// maps and neither leaks into the other (round-2 F2). Returning `None` /// rather than an empty map keeps the unfolded path byte-identical. #[must_use] pub fn map_for_window(registry: &FoldRegistry, window: &Window) -> Option { let folds = registry.folds(window.buffer_id); if folds.is_empty() { return None; } Some(VisibleLineMap::build(&folds, |off| { window.text_view.line_at_offset(off) })) } /// A maximal run of consecutive hidden source lines, plus the one /// visible line that heads it. /// /// `first_hidden >= 1` always: a component's `head_line` is /// `first_hidden - 1`, and a fold's head line is the line *above* its /// first hidden line, so line 0 can never be hidden. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct HiddenComponent { /// First hidden source line (inclusive). first_hidden: usize, /// Last hidden source line (inclusive). last_hidden: usize, /// End-of-content byte of `head_line` — the `ByteRange::start` of /// the earliest fold participating in this component, which is /// exactly where Stage 1 moves point on a fold-at-cursor. head_position: Position, } impl HiddenComponent { /// The one visible line immediately above this component. const fn head_line(&self) -> usize { self.first_hidden - 1 } } /// A buffer's collapsed regions, projected into line space. /// /// Derived from a fold list and a byte→line lookup; cheap enough to /// rebuild per window per frame (**Bet B4**: `O(folds)` with one binary /// search into the caller's existing line-offset table per fold, and /// folds are `O(top-level blocks)`). /// /// An empty map (`is_identity`) means "no folds" — callers pass `None` /// rather than an empty map so the unfolded path stays byte-identical. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct VisibleLineMap { /// Sorted by `first_hidden`, non-overlapping, and separated by at /// least one visible line (adjacency is merged away at build time). components: Vec, } impl VisibleLineMap { /// Derive the map from a buffer's folds. /// /// `line_at_offset` is the caller's own line-offset lookup (the /// rendering window's [`TextView`](crate::text_view::TextView), the /// only table guaranteed to agree with the rows being painted). A /// fold's stored range is `[end of head line, end of last hidden /// line]`, so `head_line = line_at_offset(start)` and `last_hidden = /// line_at_offset(end)`; a fold that no longer spans a whole line /// (mid-edit drift) contributes nothing. #[must_use] pub fn build(folds: &[ByteRange], line_at_offset: F) -> Self where F: Fn(Position) -> usize, { let mut raw: Vec = folds .iter() .filter_map(|f| { let head_line = line_at_offset(f.start); let last_hidden = line_at_offset(f.end); (last_hidden > head_line).then_some(HiddenComponent { first_hidden: head_line + 1, last_hidden, head_position: f.start, }) }) .collect(); raw.sort_by(|a, b| { a.first_hidden .cmp(&b.first_hidden) .then(a.last_hidden.cmp(&b.last_hidden)) }); let mut components: Vec = Vec::with_capacity(raw.len()); for c in raw { match components.last_mut() { // Overlapping OR adjacent: `c`'s head line is itself // hidden by `prev`, so it can never render — the merged // component keeps `prev`'s (visible) head. Some(prev) if c.first_hidden <= prev.last_hidden + 1 => { prev.last_hidden = prev.last_hidden.max(c.last_hidden); } _ => components.push(c), } } Self { components } } /// Whether this map hides nothing — the identity projection. #[must_use] pub fn is_identity(&self) -> bool { self.components.is_empty() } /// The component hiding `line`, if any. fn component_of(&self, line: usize) -> Option<&HiddenComponent> { let after = self.components.partition_point(|c| c.first_hidden <= line); let c = self.components.get(after.checked_sub(1)?)?; (line <= c.last_hidden).then_some(c) } /// Whether `line` is collapsed away and renders no row. #[must_use] pub fn is_hidden(&self, line: usize) -> bool { self.component_of(line).is_some() } /// Whether `line` is the visible head of a collapsed region — the /// row that carries the ellipsis and the gutter fold glyph. #[must_use] pub fn is_head(&self, line: usize) -> bool { self.components .binary_search_by(|c| c.first_hidden.cmp(&(line + 1))) .is_ok() } /// The **outermost visible head** of `line`: for a hidden line, its /// component's head line; for a visible line, itself. /// /// The **row-only** clamp — diagnostic signs, the relative-number /// cursor anchor, and the backward `view_top` clamp. Positions that /// carry a column use [`Self::visible_position`] instead. #[must_use] pub fn visible_head_of(&self, line: usize) -> usize { self.component_of(line) .map_or(line, HiddenComponent::head_line) } /// The **position** projection of a byte on `line`: for a hidden /// line, its component's `head_position` (the head line's /// end-of-content byte); for a visible line, `pos` unchanged. /// /// Used wherever a clamp carries a column — the local caret, peer /// cursors, and selection endpoints — so a hidden point lands at the /// head's end of content rather than at an arbitrary column on the /// head (round-2 F3) or at a still-hidden crossing fold's start /// (round-3 F2). #[must_use] pub fn visible_position(&self, line: usize, pos: Position) -> Position { self.component_of(line).map_or(pos, |c| c.head_position) } /// Clamp a candidate `view_top` **backward** to a visible line, so a /// fold at the top of the viewport shows its head rather than being /// skipped past (framing acceptance 8). #[must_use] pub fn clamp_view_top(&self, line: usize) -> usize { self.visible_head_of(line) } /// The next visible line strictly after `line`, skipping whole /// collapsed regions. May exceed the buffer's line count; callers /// bound it themselves. #[must_use] pub fn next_visible(&self, line: usize) -> usize { let next = line + 1; self.component_of(next) .map_or(next, |c| c.last_hidden.saturating_add(1)) } /// The previous visible line strictly before `line`, or `0` when /// `line` is already the first line. #[must_use] pub fn prev_visible(&self, line: usize) -> usize { match line.checked_sub(1) { Some(prev) => self.visible_head_of(prev), None => 0, } } /// Number of visible lines in the half-open range `[from, to)`; /// `0` when `to <= from`. /// /// This is the framing's `visible_between` — exposed unsigned and /// half-open (plus the symmetric [`Self::visible_distance`]) because /// no consumer reads the sign: row offsets always measure forward /// from `view_top`, and relative line numbers want a magnitude. #[must_use] pub fn visible_rows_between(&self, from: usize, to: usize) -> usize { if to <= from { return 0; } (to - from) - self.hidden_in(from, to) } /// Visible-line distance between `a` and `b`, either order — the /// relative/hybrid gutter number measured across collapses. #[must_use] pub fn visible_distance(&self, a: usize, b: usize) -> usize { if a <= b { self.visible_rows_between(a, b) } else { self.visible_rows_between(b, a) } } /// Hidden lines within the half-open range `[from, to)`. fn hidden_in(&self, from: usize, to: usize) -> usize { self.components .iter() .filter(|c| c.first_hidden < to && c.last_hidden >= from) .map(|c| { // `lo <= hi` holds under the filter, so this cannot // underflow. let lo = c.first_hidden.max(from); let hi = c.last_hidden.min(to - 1); hi + 1 - lo }) .sum() } /// Total visible lines in a buffer of `total_lines` source lines — /// the denominator the mode-line scroll indicator reckons in. #[must_use] pub fn visible_line_count(&self, total_lines: usize) -> usize { total_lines - self.hidden_in(0, total_lines).min(total_lines) } /// The line `n` visible steps forward from `from` (which is first /// normalized to its visible head). `n == 0` yields that head. #[must_use] pub fn nth_visible_from(&self, from: usize, n: usize) -> usize { let mut line = self.visible_head_of(from); for _ in 0..n { line = self.next_visible(line); } line } /// The line `n` visible steps back from `from` (first normalized to /// its visible head), saturating at line 0. #[must_use] pub fn nth_visible_back(&self, from: usize, n: usize) -> usize { let mut line = self.visible_head_of(from); for _ in 0..n { if line == 0 { break; } line = self.prev_visible(line); } line } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; /// A 40-line buffer of `"L\n"`-ish rows, 8 bytes each, so line /// `n` starts at `8n` and its content ends at `8n + 7`. fn line_of(offset: Position) -> usize { (offset / 8) as usize } /// The fold that hides lines `first..=last` in that fixture. fn fold(head: usize, last_hidden: usize) -> ByteRange { ByteRange { start: (head as u64) * 8 + 7, end: (last_hidden as u64) * 8 + 7, } } fn map(folds: &[ByteRange]) -> VisibleLineMap { VisibleLineMap::build(folds, line_of) } #[test] fn empty_map_is_identity() { let m = map(&[]); assert!(m.is_identity()); assert!(!m.is_hidden(5)); assert_eq!(m.visible_head_of(5), 5); assert_eq!(m.next_visible(5), 6); assert_eq!(m.visible_rows_between(0, 10), 10); } #[test] fn single_fold_hides_its_interior_only() { // head 2, hidden 3..=6. let m = map(&[fold(2, 6)]); assert!(!m.is_hidden(2)); assert!(m.is_head(2)); for line in 3..=6 { assert!(m.is_hidden(line), "line {line} should be hidden"); assert_eq!(m.visible_head_of(line), 2); } assert!(!m.is_hidden(7)); assert_eq!(m.next_visible(2), 7); assert_eq!(m.prev_visible(7), 2); // 0,1,2,7,8,9 visible in [0,10). assert_eq!(m.visible_rows_between(0, 10), 6); assert_eq!(m.visible_line_count(10), 6); } #[test] fn nested_folds_resolve_to_the_outermost_visible_head() { // Outer: head 0, hidden 1..=9. Inner: head 3, hidden 4..=6. let m = map(&[fold(0, 9), fold(3, 6)]); assert_eq!(m.visible_head_of(5), 0, "inner head 3 is itself hidden"); assert_eq!(m.visible_head_of(3), 0); assert!(m.is_head(0)); assert!(!m.is_head(3), "a hidden head renders no row"); assert_eq!(m.next_visible(0), 10); } #[test] fn shared_head_folds_merge_to_the_longer_reach() { // Two folds on head 4: one hides 5..=6, the other 5..=9. let m = map(&[fold(4, 6), fold(4, 9)]); assert_eq!(m.visible_head_of(9), 4); assert_eq!(m.next_visible(4), 10); assert_eq!(m.visible_position(9, 9 * 8 + 3), fold(4, 6).start); } #[test] fn crossing_folds_project_to_the_first_visible_head() { // Round-3 F2: A hides 1..=3 (head 0); B is headed on line 2 and // hides 3..=5. A point on line 5 is directly inside only B, but // B's head is hidden by A — it must resolve to A's head. let m = map(&[fold(0, 3), fold(2, 5)]); assert!(m.is_hidden(5)); assert_eq!(m.visible_head_of(5), 0); assert_eq!( m.visible_position(5, 5 * 8 + 4), fold(0, 3).start, "never B's still-hidden range.start" ); assert_eq!(m.next_visible(0), 6); assert!(!m.is_head(2), "B's head is hidden, so it heads nothing"); } #[test] fn adjacent_folds_merge_because_the_later_head_is_hidden() { // A hides 1..=3 (head 0); B is headed on line 3 (hidden by A) // and hides 4..=5. Lines 1..=5 collapse under head 0. let m = map(&[fold(0, 3), fold(3, 5)]); for line in 1..=5 { assert_eq!(m.visible_head_of(line), 0, "line {line}"); } assert_eq!(m.next_visible(0), 6); } #[test] fn a_visible_line_between_two_folds_keeps_them_separate() { // A hides 1..=3 (head 0); B hides 5..=6 (head 4). Line 4 stays // visible, so the components do not merge. let m = map(&[fold(0, 3), fold(4, 6)]); assert!(!m.is_hidden(4)); assert_eq!(m.visible_head_of(3), 0); assert_eq!(m.visible_head_of(6), 4); assert_eq!(m.next_visible(0), 4); assert_eq!(m.next_visible(4), 7); } #[test] fn visible_position_leaves_a_visible_byte_alone() { let m = map(&[fold(2, 6)]); assert_eq!(m.visible_position(7, 7 * 8 + 2), 7 * 8 + 2); } #[test] fn clamp_view_top_goes_backward_to_the_head() { let m = map(&[fold(2, 6)]); assert_eq!(m.clamp_view_top(5), 2); assert_eq!(m.clamp_view_top(2), 2); assert_eq!(m.clamp_view_top(7), 7); } #[test] fn visible_distance_is_symmetric_and_skips_folds() { let m = map(&[fold(2, 6)]); // Visible order: 0,1,2,7,8 — line 8 is 4 visible steps from 0. assert_eq!(m.visible_distance(0, 8), 4); assert_eq!(m.visible_distance(8, 0), 4); assert_eq!(m.visible_distance(2, 7), 1); } #[test] fn nth_visible_walks_forward_and_back_over_folds() { let m = map(&[fold(2, 6)]); assert_eq!(m.nth_visible_from(0, 3), 7); assert_eq!(m.nth_visible_back(8, 4), 0); // A hidden origin normalizes to its head first. assert_eq!(m.nth_visible_from(5, 1), 7); assert_eq!(m.nth_visible_back(5, 1), 1); } #[test] fn build_drops_a_fold_that_no_longer_spans_a_line() { // start and end inside one line: nothing to hide. let degenerate = ByteRange { start: 10, end: 12 }; assert!(map(&[degenerate]).is_identity()); } }