// view.rs --- The View trait. Polymorphic interpretive layer over a buffer. //! Views: the polymorphic interpretive layer attached to a buffer. //! //! Implements the view contract from spec §3.3 (Views). A view subscribes //! to edits, maintains derived state, contributes to rendering, and may //! intercept edits before they reach the rope. //! //! # Re-entry by construction //! //! Views never hold a `&Buffer` or `&mut Buffer` of their own. The buffer //! passes itself into every callback (per spec §2.6, Checkpoint 6). While a //! callback runs, the buffer's `&mut self` is held by the buffer's own //! stack frame; no aliasing reference exists for the view to use, so a view //! cannot recursively call `buffer.apply_edit` from inside its own //! `on_edit` --- the borrow checker rejects it. //! //! # Composition (T M2.9) //! //! A window holds one base [`crate::text_view::TextView`] plus a stack //! of overlay views (`window.overlays`). The frontend renders them in //! a deterministic order: //! //! 1. The base text view runs first. It clears every cell in the //! window's viewport and paints buffer text + default style. //! 2. Overlays then run in attach order (FIFO). Each overlay //! observes the cells already written and may read, mutate in //! place, or replace any cell inside the viewport. //! 3. At any cell two views both touch, the later writer wins on the //! fields it chooses to write. Overlays are free to read existing //! fields and merge — see [`crate::overlay::StyleSpanOverlay`] for //! a style-merging overlay and [`crate::overlay::VirtualCellOverlay`] //! for a whole-cell-replacement overlay. //! //! Cursor placement and scrolling use the base text view's //! [`View::pos_to_display`] only; overlays do not move the cursor. //! Composition adds well under 10% overhead over a single-view render //! when overlays touch only the cells they declare (see //! `composition_overhead_under_ten_percent` in `editor.rs`). use crate::buffer::{Buffer, BufferError, BufferId, EditOp}; use crate::cell::{CellCoord, CellGrid, CellSize}; use crate::fold_view::VisibleLineMap; use crate::rope::{Edit, Position}; // --------------------------------------------------------------------------- // InterceptContext (T M7.4) // --------------------------------------------------------------------------- /// Snapshot of the buffer's identity and shape, passed to /// [`View::intercept_edit`] in lieu of a `&Buffer` reference. /// /// # Why a snapshot, not a `&Buffer` /// /// Pre-M7.4, `intercept_edit` received `&Buffer`. The Lua-bindings /// layer held the registry's `RefCell::borrow_mut` for the full /// duration of the call, so an intercept body that re-entered /// `pmacs.buffer.X` synchronously hit a recursive-borrow error /// (`BindingError::Reentrant`); the M6.10 audit fix surfaced this as /// a typed error rather than a panic but did not enable the re-entry. /// /// M7.4 splits the edit flow into three phases. Phase 2 runs the /// intercept chain with the registry borrow released, so an intercept /// body may call back into `pmacs.buffer.X` on any buffer. The cost: /// the intercept can no longer hold a `&Buffer` (the buffer might be /// mutated by the re-entrant call). Instead, we hand it an /// `InterceptContext` snapshot taken at the start of the edit; the /// fields cover every read the intercept needs (`buf_id` for routing, /// `buf_len` for clamping positions, `buf_name` for diagnostic /// messages, `revision` for "did this snapshot drift?" checks). #[derive(Clone, Debug, Eq, PartialEq)] pub struct InterceptContext { /// The buffer's identifier. Stable across the edit. pub buf_id: BufferId, /// Buffer length in bytes at the moment the snapshot was taken. pub buf_len: u64, /// Buffer name at the moment the snapshot was taken. pub buf_name: String, /// Buffer revision at the moment the snapshot was taken. Useful /// for re-entrant cross-buffer edits to detect whether the parent /// buffer was mutated during their lifetime. pub revision: u64, } impl InterceptContext { /// Build a context snapshot from a buffer reference. #[must_use] pub fn snapshot(buf: &Buffer) -> Self { Self { buf_id: buf.id(), buf_len: buf.len(), buf_name: buf.name().to_string(), revision: buf.revision(), } } } // --------------------------------------------------------------------------- // Coordinates // --------------------------------------------------------------------------- /// Coordinate in display space (row, col), measured in cells. /// /// Distinct from [`CellCoord`] in name only at this stage, but kept separate /// for documentation: `DisplayCoord` is a *content* coordinate (reported by /// `pos_to_display`), `CellCoord` is a *grid* coordinate (where to draw). /// They coincide for plain text but diverge once virtual lines, wrapping, /// and inline expansions appear. #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] pub struct DisplayCoord { /// 0-based **source line** index. /// /// Deliberately still the source line under wrapping, not a visual /// row. Redefining it would have broken every existing consumer — /// `overlay_paint`'s `row - view_top`, vertical motion's bounds /// check — with no compile error. Adding [`Self::sub_row`] beside it /// instead leaves those consumers *correct*, not merely findable. pub row: u32, /// Which visual row **within** `row`, when the line wraps. /// /// `0` for every unwrapped line and under /// [`WrapMode::Truncate`](crate::view::WrapMode::Truncate), which is /// what makes this field additive: code that has never heard of /// wrapping keeps computing the right answer. pub sub_row: u32, /// 0-based column, within the visual row named by `sub_row`. pub col: u32, } impl DisplayCoord { /// Construct a display coordinate on a line's first visual row. #[must_use] pub const fn new(row: u32, col: u32) -> Self { Self { row, sub_row: 0, col, } } /// Construct a display coordinate on a specific visual row of a /// wrapped line. #[must_use] pub const fn wrapped(row: u32, sub_row: u32, col: u32) -> Self { Self { row, sub_row, col } } } /// The layout facts a coordinate mapping needs, which the mapping /// itself cannot know. /// /// # Why this is a required parameter /// /// `pos_to_display` took `(&self, buf, pos)` and had no notion of the /// grid at all — so under wrapping it could not compute a visual row, /// and `display_to_pos` could not invert one. Passing the missing /// facts as a required argument is deliberate: it makes the compiler /// enumerate every call site rather than leaving an audit to grep. /// /// That is the opposite choice from [`DisplayCoord::sub_row`], and for /// the opposite reason. Enforcement is possible on the way in, so it is /// taken; it is not possible on the way out, so the output is made /// correct-by-default instead. #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] pub struct LayoutCtx { /// Width in cells of the content area the text wraps within. /// /// `0` means "not rendered yet" — the same convention /// `Window::last_visible_rows` uses — and is treated as unwrapped, /// since a viewport with no columns has no rows to distinguish. pub cols: u32, /// The window's resolved wrap mode. pub wrap: WrapMode, /// First visible display column — the window's horizontal scroll /// offset (Stage 4, framing Q#HS7). /// /// **Stored unsnapped.** It is one per-window column, while "does /// this column bisect a wide glyph?" is a *per-line* question, so no /// single snapped value could be canonical for every visible line. /// Each line derives its own **effective edge** during the walk it /// already performs from column 0 (framing Q#HS7(c′)). /// /// Inert under [`WrapMode::Wrap`]: a wrapped line has nothing past /// the right edge to scroll toward, so the wrap path ignores this /// entirely and stays byte-identical to Stage 3. pub view_left: u32, } impl LayoutCtx { /// The identity context: no wrapping, width irrelevant. /// /// Every pre-wrap caller means this, and saying so explicitly is /// what makes those call sites readable as decisions rather than /// oversights. #[must_use] pub const fn truncated() -> Self { Self { cols: 0, wrap: WrapMode::Truncate, view_left: 0, } } /// Whether this context actually wraps. #[must_use] pub const fn wrapping(self) -> bool { matches!(self.wrap, WrapMode::Wrap) && self.cols > 0 } /// The horizontal offset that actually applies. /// /// Always `0` when wrapping, which is what makes `view_left` inert /// under `wrap` **by construction** rather than by every caller /// remembering to check. A wrapped line has no content past the /// right edge, so a non-zero offset there could only hide text that /// nothing would ever scroll back to. #[must_use] pub const fn effective_left(self) -> u32 { if self.wrapping() { 0 } else { self.view_left } } } /// How a line wider than the viewport is shown --- the long-lines /// stage, `docs/long-lines-framing.md`. /// /// # Why the renderer is told, rather than asking /// /// This is the *resolved* mode, not the setting's name. `ui.line-wrap` /// is **buffer-local** (framing Q#LL2), and the config registry has no /// ambient current buffer by design (`config_registry.rs`, Q#CR4) — a /// caller wanting buffer-aware behavior must pass the `BufferId`. The /// render driver holds both the registry and the buffer, resolves once /// per window per frame, and puts the answer here. Views stay /// config-agnostic, exactly as they do for folds. /// /// # `Truncate` is the identity case, deliberately /// /// Every behavior predating this type is `Truncate`, and it must stay /// byte-identical under it — which is what lets the wrap work be /// verified against the existing suite rather than against new /// assertions. #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Hash)] pub enum WrapMode { /// One source line per row; the remainder is clipped at the right /// edge. What every pre-Stage-3 caller did. #[default] Truncate, /// A line longer than the viewport continues on the following rows. /// /// Character wrap, not word wrap (framing Q#LL5): it matches /// Emacs's default, and it is the only break rule both frontends /// can implement identically without pulling UAX #14 into the grid. Wrap, } /// What to render and where. /// /// The frontend computes the viewport (which buffer range maps to which /// cell origin) and hands it to the view. The view fills cells inside that /// origin/size window. #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct Viewport<'a> { /// First byte in the buffer to consider rendering. pub buffer_start: Position, /// One past the last byte to consider. pub buffer_end: Position, /// Top-left cell in the grid where rendering should start. pub cell_origin: CellCoord, /// Number of cells the viewport occupies. pub cell_size: CellSize, /// Width of the line-number gutter reserved to the *left* of /// `cell_origin` (UX gutter arc). `0` when no gutter. Overlays that /// want to draw in the gutter (e.g. the diagnostic sign) reach it at /// `cell_origin.col - gutter_w`; overlays that only touch the text area /// ignore it (the origin is already shifted past the gutter). pub gutter_w: u32, /// The rendering window's collapsed regions (Arc 6 Stage 2, Q#FD12), /// or `None` when this window's buffer has no folds — the unfolded /// path then stays byte-identical to the pre-folding renderer. /// /// Borrowed rather than owned so `Viewport` stays [`Copy`]: the frame /// builds **one map per rendered window** (never one per frame — a /// split may show different buffers) and hands the same shared /// reference to every painter of that window. pub folds: Option<&'a VisibleLineMap>, /// How lines wider than `cell_size.cols` are shown, already /// resolved for this window's buffer (see [`WrapMode`]). /// /// A required field rather than a defaulted one on purpose: every /// construction site has to state which behavior it means, so the /// pre-Stage-3 sites read as *deliberately* unwrapped rather than /// merely untouched. pub wrap: WrapMode, /// First visible display column (Stage 4). See /// [`LayoutCtx::view_left`]; required rather than defaulted for the /// same reason `wrap` is. pub view_left: u32, } impl Viewport<'_> { /// The horizontal offset that actually applies — `0` when wrapping, /// for the reason [`LayoutCtx::effective_left`] gives. #[must_use] pub const fn left_edge(&self) -> u32 { if matches!(self.wrap, WrapMode::Wrap) { 0 } else { self.view_left } } /// Clip a **line**-column range to what is on screen, returning /// **screen** columns — or `None` when none of it is visible. /// /// # Why every buffer-coordinate decorator must use this /// /// Stage 4 translated the base text walk and nothing else, which /// split the frame in half: at `view_left = 10` a glyph at source /// column 10 painted at screen column 0 while its syntax style, /// diagnostic underline, search wash and selection painted at screen /// column 10 — or vanished. Decorations drifted off the characters /// they describe, silently, and only once a window was scrolled. /// /// Every such site had the same two lines (`start_col.min(max_cols)`, /// `end_col.min(max_cols)`) — correct only while the left edge was /// pinned at zero. One helper replaces all of them so a future /// decorator inherits the translation instead of re-deriving it. /// /// **Five adopters**, and the count is the point: syntax/LSP /// styling, diagnostic underlines, search washes, /// [`crate::overlay::BufferStyleOverlay`], and the selection /// painter. The selection was nearly the exception — Stage 4's first /// version duplicated the rule there, justified by a width this /// painter supposedly needed and the viewport lacked. That was /// false: the render viewport's `cell_size.cols` is already /// `rect.size.cols - gutter_w`, and its origin already sits past the /// gutter. A canonical rule with one honest exception is not /// canonical, so the exception went. /// /// **Not for [`crate::overlay::StyleSpanOverlay`] or /// [`crate::overlay::VirtualCellOverlay`]**: those are documented as /// viewport-relative, so their columns are already screen columns /// and translating them twice would be the mirror defect. #[must_use] pub fn visible_cols(&self, start_col: u32, end_col: u32) -> Option<(u32, u32)> { let left = self.left_edge(); let right = left.saturating_add(self.cell_size.cols); let start = start_col.max(left); let end = end_col.min(right); // A range that begins off-screen left and reaches past the edge // is CLIPPED, not skipped — that is the selection defect this // returns `Some` for. (end > start).then(|| (start - left, end - left)) } /// Row offset within this viewport for source `line`, given the /// viewport's first (visible) source line. /// /// `None` when `line` is above `start_line` or collapsed away — a /// hidden line simply has no row, so its painter skips it. Without a /// map this is the identity `line - start_line` every consumer used /// before folding. /// /// The result is monotonically non-decreasing in `line`, so a caller /// that bounds its walk with `row_offset >= rows` may still `break`. #[must_use] pub fn row_offset_of(self, start_line: usize, line: usize) -> Option { let raw = line.checked_sub(start_line)?; let rows = match self.folds { Some(map) if !map.is_identity() => { if map.is_hidden(line) { return None; } map.visible_rows_between(start_line, line) } _ => raw, }; u32::try_from(rows).ok() } /// The inverse of [`Self::row_offset_of`]: the source line rendered /// at `row_offset`, given the viewport's first (visible) source line. /// /// Painters that walk rows rather than spans (the syntax and LSP /// style views) use this; the result may exceed the buffer's line /// count, which those callers already bound. #[must_use] pub fn line_at_row_offset(self, start_line: usize, row_offset: u32) -> usize { match self.folds { Some(map) if !map.is_identity() => { map.nth_visible_from(start_line, row_offset as usize) } _ => start_line + row_offset as usize, } } } // --------------------------------------------------------------------------- // Trait // --------------------------------------------------------------------------- /// Polymorphic interpretive layer attached to a buffer. /// /// All callbacks receive `&Buffer` rather than `&mut Buffer`: views observe /// and contribute, they do not mutate the buffer mid-callback. To produce /// edits, a view returns a transformed [`EditOp`] from `intercept_edit` --- /// or none of the above, and instead schedules a worker to do expensive work /// asynchronously (M3+). /// /// # Threading /// /// Views run on the main thread. The buffer they attach to lives behind /// `Rc>` and is not sent across threads (workers /// receive rope snapshots, not buffer or view references). The trait /// therefore does not require `Send`; this lets views hold types that /// are not `Send` themselves --- notably `mlua` handles, used by the /// M6.4 REPL package's `LuaInterceptView` to attach a Lua callback as /// an intercept-edit chain entry. pub trait View { /// Validate or transform a proposed edit before it reaches the rope. /// /// Returning the input as-is passes through; returning a different /// [`EditOp`] rewrites the edit (used by, e.g., a directory-listing /// view that translates filename edits into rename operations). /// Returning an error rejects the edit. /// /// The view receives a [`InterceptContext`] snapshot of the /// buffer's identity and shape at the moment the edit began /// (T M7.4). Intercept bodies needing live state on another /// buffer can re-enter `pmacs.buffer.X` (or the in-process /// equivalent); the registry borrow is released for the duration /// of this call. /// /// Default: pass through. fn intercept_edit<'a>( &mut self, _ctx: &InterceptContext, op: EditOp<'a>, ) -> Result, BufferError> { Ok(op) } /// Called after every successful edit. /// /// The view updates its derived state. Must be cheap; expensive work /// is dispatched to a worker (M3+) and reflected back via a separate /// update path. /// /// Default: no-op. fn on_edit(&mut self, _buf: &Buffer, _edit: &Edit) -> Result<(), BufferError> { Ok(()) } /// Render the buffer region named by `viewport` into `cells`. /// /// Pure-ish: same inputs produce the same outputs. The view writes only /// inside `viewport.cell_origin .. cell_origin + cell_size`; cells /// outside that window are left untouched (composition is the /// frontend's job). /// /// Default: no-op (used by views that participate in `on_edit` but not /// in rendering). fn render(&mut self, _buf: &Buffer, _viewport: Viewport<'_>, _cells: &mut CellGrid<'_>) {} /// Translate a buffer byte position to a display coordinate, if the /// view holds a meaningful mapping for that position. /// /// Default: returns `None` (view has no opinion). fn pos_to_display( &self, _buf: &Buffer, _pos: Position, _ctx: LayoutCtx, ) -> Option { None } /// Translate a display coordinate back to a buffer byte position. /// /// Default: returns `None`. fn display_to_pos( &self, _buf: &Buffer, _coord: DisplayCoord, _ctx: LayoutCtx, ) -> Option { None } /// Stable identifier for this view's *kind*. Used by introspection /// seams (e.g. `pmacs.window._overlay_kinds()`) to verify that a /// specific overlay type actually attached after a wire-up step, /// without needing `Any` downcasts. The default `"unknown"` is /// fine for views that no test cares about; views that participate /// in cross-package wiring (syntax highlight, style overlays) /// should override. fn kind(&self) -> &'static str { "unknown" } /// Identity of the shared resource this overlay renders, if any /// (PR #113 round-6 findings 1 and 3). Two overlay instances /// backed by the same store report the same value, which lets a /// window attach a resource-backed overlay AT MOST once /// ([`crate::window::Window::ensure_overlay`]) and lets disposal /// remove every window copy. The default `None` opts out: such /// views are never deduplicated or bulk-removed. fn overlay_identity(&self) -> Option { None } /// A copy of this overlay for a freshly split window showing the /// same buffer (round-6 finding 1: a same-buffer split starts /// with an empty overlay list and fires no switch hook, so /// without this the new pane rendered unstyled). `None` (the /// default) means the view does not carry across splits; /// store-backed render overlays return a clone. fn clone_for_split(&self) -> Option> { None } /// Retarget this overlay from `old_uri` to `new_uri` after a /// resource rename (dired Stage 2a, §5). Default: no-op — a view /// that renders nothing URI-keyed is unaffected. /// /// Mutates **in place**, so the overlay keeps its position in the /// window's composition order. That is the reason this is a trait /// hook rather than a remove-and-re-push at the call site: overlays /// are an ordered `Vec` merged in sequence, and re-pushing would /// move a diagnostic underline to the end of the stack. It is also /// how *passive* windows are reached at all — the Lua attach path /// (`pmacs.diag._attach_view`) can only touch the active window, /// while the sweep that drives this walks every window. fn rename_resource(&mut self, _old_uri: &str, _new_uri: &str) {} } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn display_coord_basics() { let c = DisplayCoord::new(3, 5); assert_eq!(c.row, 3); assert_eq!(c.col, 5); } #[test] fn viewport_is_copy() { // Compile-time assertion: Viewport must be Copy so the frontend can // hand the same descriptor to multiple views without ceremony. Arc 6 // Stage 2 (Bet B7) keeps this true by borrowing the fold map — a // shared reference is itself `Copy`. fn assert_copy() {} assert_copy::>(); } #[test] fn row_offset_without_folds_is_the_identity_map() { let vp = Viewport { buffer_start: 0, buffer_end: 0, cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(10, 10), gutter_w: 0, folds: None, wrap: WrapMode::Truncate, view_left: 0, }; assert_eq!(vp.row_offset_of(4, 4), Some(0)); assert_eq!(vp.row_offset_of(4, 9), Some(5)); assert_eq!(vp.row_offset_of(4, 3), None, "above the viewport"); } #[test] fn row_offset_skips_hidden_lines_and_compacts_rows() { // Fold heading line 1, hiding lines 2..=4 (8-byte lines). let map = VisibleLineMap::build(&[pmacs_protocol::ByteRange { start: 15, end: 39 }], |off| { (off / 8) as usize }); let vp = Viewport { buffer_start: 0, buffer_end: 0, cell_origin: CellCoord::new(0, 0), cell_size: CellSize::new(10, 10), gutter_w: 0, folds: Some(&map), wrap: WrapMode::Truncate, view_left: 0, }; assert_eq!(vp.row_offset_of(0, 1), Some(1), "the head keeps its row"); assert_eq!(vp.row_offset_of(0, 3), None, "hidden lines have no row"); assert_eq!(vp.row_offset_of(0, 5), Some(2), "rows below shift up"); } /// C9: §4 of `docs/crdt-identity-undo-framing.md` enumerates every /// in-tree `on_edit` override so that the consumers of a /// version-only history `Edit` are a closed set. This asserts the /// set is still what the census measured. /// /// **It asserts PAIRS, not a file set and a count.** Replacing /// `ParseView`'s override with an unclassified type in the same /// file leaves both the file set and the total unchanged, and only /// the pair set catches it. /// /// **Its reach is in-tree, and that is a real limit.** [`View`] and /// `Buffer::attach_view` are both public, so a downstream crate may /// implement `on_edit` and attach it; no in-tree measurement can /// enumerate that. What speaks to those implementors is the /// documented contract on `Edit` itself. /// /// It also guards only the census's CLOSURE condition — that the /// override set is unchanged — not the classifications inside it. /// Those are executed by /// `identity_replace_history_op_leaves_classified_consumers_unchanged`. #[test] fn every_in_tree_on_edit_override_is_one_the_census_classified() { /// `(file, impl target)`, as measured by the census. const CLASSIFIED: [(&str, &str); 4] = [ ("fold.rs", "FoldStoreTranslator"), ("overlay.rs", "BufferStyleSpanTranslator"), ("syntax.rs", "ParseView"), ("text_view.rs", "TextView"), ]; fn rs_files(dir: &std::path::Path, out: &mut Vec) { for entry in std::fs::read_dir(dir).expect("src is readable") { let path = entry.expect("dir entry").path(); if path.is_dir() { rs_files(&path, out); } else if path.extension().is_some_and(|e| e == "rs") { out.push(path); } } } let mut files = Vec::new(); rs_files( &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut files, ); files.sort(); let mut found: Vec<(String, String)> = Vec::new(); for path in &files { let name = path .file_name() .expect("file name") .to_string_lossy() .into_owned(); let text = std::fs::read_to_string(path).expect("source is readable"); let lines: Vec<&str> = text.lines().collect(); // The test module boundary: the first `#[cfg(test)]` that // introduces a `mod`. Overrides below it are test fixtures // and are out of scope, as the census says. let boundary = lines.iter().enumerate().find_map(|(i, l)| { (l.trim() == "#[cfg(test)]" && lines .get(i + 1) .is_some_and(|n| n.trim_start().starts_with("mod "))) .then_some(i) }); for (i, line) in lines.iter().enumerate() { if boundary.is_some_and(|b| i > b) { break; } if !line.contains("fn on_edit") { continue; } // Walk back to the enclosing `impl … for `. A hit // on a trait declaration first means this is the // trait's own default, which is not an override. for j in (0..=i).rev() { let l = lines[j].trim_start(); if let Some(rest) = l.strip_prefix("impl") && let Some(after) = rest.split(" for ").nth(1) { let target = after .split_whitespace() .next() .unwrap_or("") .trim_end_matches('{') .rsplit("::") .next() .unwrap_or("") .to_owned(); found.push((name.clone(), target)); break; } if l.starts_with("trait ") || l.starts_with("pub trait ") { break; } } } } found.sort(); let expected: Vec<(String, String)> = CLASSIFIED .iter() .map(|(f, t)| ((*f).to_owned(), (*t).to_owned())) .collect(); assert_eq!( found, expected, "C9: the in-tree `on_edit` override set no longer matches the \ census in docs/crdt-identity-undo-framing.md §4. Every \ override is a consumer of the version-only history `Edit` \ and needs classifying there." ); } }