Skip to main content

lattice_core/
folding.rs

1//! Buffer-level folding semantics.
2//!
3//! M.7 adds `FoldSource` + `FoldOverlayService` — a dep-safe
4//! bridge that lets subsystems below `lattice-host` (e.g.
5//! `lattice-multibuffer`) register overlay fold providers
6//! without depending on `FoldContext` or `FoldProvider`.
7//!
8//! [`FoldMethod`] decides which provider feeds the per-buffer fold
9//! list. [`Fold`] is the per-range entry the list holds. Both are
10//! renderer-agnostic: a fold is just `(start_line, end_line,
11//! closed, identity)`; the gutter glyph + summary-line text are
12//! rendering concerns layered on top.
13
14crate::labeled_enum! {
15    /// `:set foldmethod=...` (DESIGN.md §15:18, C.2;
16    /// `docs/user/folding.md`). Decides which provider feeds the
17    /// per-buffer fold list.
18    ///
19    /// Each variant's marginalia doc (right of `=>`) is what
20    /// appears in `:set foldmethod=<Tab>`. Variant-level `///`
21    /// docs are for API/rustdoc consumers. Slice
22    /// `3c.unify.option-docs-builtin` migrated this enum to
23    /// `labeled_enum!` — adding a new fold method is now one
24    /// line; the `label` / `parse_label` / `doc` / `all`
25    /// accessors are derived automatically.
26    ///
27    /// D.3.f.0 added `#[derive(Hash)]` so the `FoldRegistry`
28    /// can key its primary-provider map on `FoldMethod`.
29    #[derive(Hash)]
30    pub enum FoldMethod {
31        /// Only user `zf` ranges, no auto-recompute.
32        #[default]
33        Manual = "manual"
34            => "User-defined folds only (zf to create, zd to delete)",
35        /// Universal indent walker.
36        Indent = "indent"
37            => "Fold by indent level",
38        /// ATX heading nesting (`*.md`).
39        Markdown = "markdown"
40            => "Fold by markdown headings (#, ##, ###, …)",
41        /// Tree-sitter scope queries; cascades to `Markdown` for
42        /// `.md` buffers and `Indent` otherwise when the tree-
43        /// sitter provider has nothing to offer.
44        Syntax = "syntax"
45            => "Folds from the tree-sitter syntax tree",
46        /// Feeds from `textDocument/foldingRange`. Async:
47        /// the per-tick pump fires the request when the buffer's
48        /// document version changes; the response lands in a
49        /// per-buffer cache and triggers a recompute. Cascades to
50        /// `Syntax` when no attached server advertises the
51        /// capability.
52        ///
53        /// Slice: 4.4.f.
54        Lsp = "lsp"
55            => "Folds from LSP `textDocument/foldingRange`",
56    }
57}
58
59/// One contiguous fold range in a document buffer.
60///
61/// `identity` is the stable handle used to carry closed-state
62/// across recomputes. Computed providers (indent / markdown) hash
63/// the trimmed start-line text together with the leading-indent
64/// depth so that adding or removing lines elsewhere in the buffer
65/// doesn't reopen this fold. Manual folds (`zf`) leave it `None`
66/// -- their stable identity is the line range itself.
67///
68/// Phase 5.2: moved from `lattice-ui-tui::app::Fold` to this
69/// renderer-agnostic home. Existing `crate::app::Fold` call sites
70/// continue to resolve via a `pub use lattice_core::Fold;`
71/// re-export in `lattice-ui-tui::app`.
72#[derive(Debug, Clone, Copy)]
73pub struct Fold {
74    /// First line of the fold, 0-based. Stays visible when the fold is
75    /// closed (it carries the summary).
76    pub start_line: u32,
77    /// Last line of the fold, 0-based and **inclusive**. Providers only emit
78    /// folds with `end_line > start_line`.
79    pub end_line: u32,
80    /// Whether the fold is collapsed, hiding `start_line + 1 ..= end_line`.
81    pub closed: bool,
82    /// Stable identity used to carry `closed` across recomputes; `None` for
83    /// manual folds, whose identity is the line range. See the type docs.
84    pub identity: Option<u64>,
85}
86
87/// Distinguishes mutually-exclusive primary fold sources
88/// (one runs at a time, picked by `:set foldmethod=`) from
89/// additive overlay sources (always compose). See
90/// `docs/dev/architecture/fold-architecture.md` §2.
91///
92/// Slice: D.3.f.0.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum ProviderKind {
95    /// Selected by [`FoldMethod`]; exactly one primary provider feeds a
96    /// buffer at a time.
97    Primary,
98    /// Always composed on top of the primary folds (e.g. multibuffer
99    /// excerpt folds), regardless of `foldmethod`.
100    Overlay,
101}
102
103/// Stable identifier for a registered fold provider.
104/// Two distinct providers must produce distinct ids; a single
105/// provider produces the same id across recomputes. Used by the
106/// registry for lookup and by diagnostics that need to attribute
107/// a fold back to its source.
108///
109/// Slice: D.3.f.0.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
111pub struct ProviderId(pub u64);
112
113/// Data-only fold source for subsystems that live below
114/// `lattice-host` (e.g. `ExcerptFoldProvider` in
115/// `lattice-multibuffer`). Implementors cannot depend on
116/// `FoldContext` or `FoldProvider` (both defined in `lattice-host`).
117/// `FoldSourceAdapter` in `lattice-host::fold_provider` wraps any
118/// `FoldSource` as a `FoldProvider` by delegating `compute()` to
119/// `compute_folds` and ignoring `FoldContext`.
120///
121/// Slice: M.7.
122pub trait FoldSource: Send + Sync {
123    /// This source's stable id: the same on every call, and distinct from
124    /// every other registered source's (see [`ProviderId`]).
125    fn id(&self) -> ProviderId;
126    /// Produce the current fold ranges. Called by the host on recompute, so
127    /// it should be cheap (read already-computed state; no I/O).
128    fn compute_folds(&self) -> Vec<Fold>;
129}
130
131/// Service for registering / deregistering overlay fold sources.
132/// Implemented by `FoldOverlayServiceImpl` in `lattice-host` (which
133/// wraps `Arc<Mutex<FoldRegistry>>`). Registered in the
134/// `ServiceRegistry` at boot so `MultibufferMode::on_activate` can
135/// call `add_source` without depending on `lattice-host`.
136///
137/// `buffer_id` scopes the overlay: `FoldSourceAdapter` in
138/// `lattice-host` only calls `compute_folds` when
139/// `FoldContext::buffer_id` matches, so providers from multiple
140/// simultaneous multibuffers don't bleed into each other's views.
141///
142/// Slice: M.7.
143pub trait FoldOverlayService: Send + Sync {
144    /// Register `source` as an overlay provider scoped to `buffer_id` and
145    /// return the id to pass to [`Self::remove_source`] later.
146    fn add_source(
147        &self,
148        source: std::sync::Arc<dyn FoldSource>,
149        buffer_id: crate::BufferId,
150    ) -> ProviderId;
151    /// Deregister a source added by [`Self::add_source`]. Removing an
152    /// unknown id is a no-op.
153    fn remove_source(&self, id: ProviderId);
154}
155
156/// Cheap-clone handle for the fold-overlay service. Mode guards hold
157/// one Arc so they can call `remove_source` on Drop.
158pub type FoldOverlayServiceHandle = std::sync::Arc<dyn FoldOverlayService>;
159
160#[cfg(test)]
161mod tests {
162    #![allow(clippy::unwrap_used, clippy::panic)]
163    use super::*;
164
165    #[test]
166    fn label_round_trips_through_parse_label() {
167        for fm in [
168            FoldMethod::Manual,
169            FoldMethod::Indent,
170            FoldMethod::Markdown,
171            FoldMethod::Syntax,
172            FoldMethod::Lsp,
173        ] {
174            assert_eq!(FoldMethod::parse_label(fm.label()), Ok(fm));
175        }
176    }
177
178    #[test]
179    fn parse_label_rejects_unknown_with_helpful_message() {
180        let err = FoldMethod::parse_label("xyz").unwrap_err();
181        assert!(err.contains("expected `manual`"));
182        assert!(err.contains("xyz"));
183    }
184
185    #[test]
186    fn default_is_manual() {
187        assert_eq!(FoldMethod::default(), FoldMethod::Manual);
188    }
189}