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}