Skip to main content

lattice_host/
folds.rs

1//! Computed folds (DESIGN.md §5.1, §15:18; user-facing reference
2//! at `docs/user/folding.md`).
3//!
4//! Folds derived automatically from the buffer's structure. v1
5//! providers:
6//!
7//! 1. **Indent** -- fold spans any line whose successor indents
8//!    deeper, ending at the last line whose indent is strictly
9//!    greater than the start. Universal across languages.
10//! 2. **Markdown** -- `^#+ ` headings define the fold tree.
11//!    A `# H1` folds until the next `# H1`; a `## H2` folds until
12//!    the next same-or-higher heading.
13//! 3. **Syntax (tree-sitter)** -- runs the language's compiled
14//!    `folds.scm` query against the parse tree owned by
15//!    [`lattice_syntax::Syntax`]. Each `@fold` capture becomes a
16//!    fold spanning the captured node's lines. Falls back to the
17//!    Markdown / Indent providers for languages that don't ship a
18//!    `folds.scm` yet.
19//!
20//! Manual folds (created via `zf` from a Visual selection) and
21//! computed folds coexist in [`crate::app::App::folds`] with no
22//! distinction at the storage layer; the `:set foldmethod` option
23//! decides which side feeds in.
24//!
25//! Fold identity (`Fold::identity`) is the SHA-style hash of the
26//! trimmed start-line text plus indent depth (for indent / markdown
27//! providers) or `(node_kind, trimmed start-line text)` (for the
28//! syntax provider). When the buffer changes and folds recompute,
29//! we match new folds to old ones by identity and transfer the
30//! closed-state -- so adding a line to one section doesn't reopen
31//! the closed section above. Manual folds carry `identity = None`
32//! (their stable identity is the line range itself).
33
34use std::hash::{DefaultHasher, Hash, Hasher};
35
36use lattice_core::Buffer;
37use lattice_core::IndentUnit;
38use lattice_syntax::SyntaxSnapshot;
39use streaming_iterator::StreamingIterator;
40use tree_sitter::QueryCursor;
41
42use lattice_core::Fold;
43
44/// Compute the stable identity hash for a computed fold.
45///
46/// Inputs are `(trimmed start-line text, indent depth)` -- enough
47/// to keep a heading distinct from sibling headings while ignoring
48/// trailing-line additions. Used by [`crate::app::App::recompute_folds`]
49/// to carry the closed/open state across edits.
50pub(crate) fn fold_identity(start_line_text: &str, indent_depth: usize) -> u64 {
51    let mut h = DefaultHasher::new();
52    start_line_text.trim().hash(&mut h);
53    indent_depth.hash(&mut h);
54    h.finish()
55}
56
57/// Compute the stable identity hash for a tree-sitter-driven fold.
58/// Uses `(node_kind, trimmed start-line text)` -- node kind separates
59/// e.g. an `impl` block from a `fn` block that happen to start with
60/// the same text, which is more stable than indent depth across
61/// reformat / refactor operations.
62fn syntax_fold_identity(node_kind: &str, start_line_text: &str) -> u64 {
63    let mut h = DefaultHasher::new();
64    node_kind.hash(&mut h);
65    start_line_text.trim().hash(&mut h);
66    h.finish()
67}
68
69/// Run the indent-based fold algorithm against `buffer` and return every fold
70/// it discovers. All produced folds are open (`closed = false`) by default --
71/// vim's `foldlevelstart` would override that, but v1 doesn't model the level
72/// option yet.
73///
74/// The walk itself lives in [`lattice_core::indent_blocks::indent_regions`],
75/// shared with indentation guides. A fold and a guide answer the same
76/// question -- *what block is this line in* -- and a user who folds a block
77/// and sees a different extent than the one just highlighted has been told two
78/// different things by the same editor.
79///
80/// **IG.5: depth is display columns now, not leading whitespace characters.**
81/// The `leading_indent` this replaced counted a tab as one column, so a
82/// tab-indented file folded at different boundaries than its space-indented
83/// twin -- the refinement the old doc comment promised "when we honour
84/// `tabstop` / `shiftwidth`". `unit` supplies that, resolved through the
85/// buffer-local stack like every other indent consumer.
86///
87/// The output is sorted by start_line; nested folds appear inside their
88/// parent's range.
89pub fn compute_indent_folds(buffer: &Buffer, unit: &IndentUnit) -> Vec<Fold> {
90    let text = buffer.as_string();
91    let lines: Vec<&str> = text.split('\n').collect();
92    if lines.len() <= 1 {
93        return Vec::new();
94    }
95    let indents = lattice_core::indent_blocks::line_indents(lines.iter().copied(), unit);
96    lattice_core::indent_blocks::indent_regions(&indents)
97        .into_iter()
98        .map(|region| Fold {
99            start_line: region.start_line,
100            end_line: region.end_line,
101            closed: false,
102            identity: Some(fold_identity(
103                lines[region.start_line as usize],
104                region.opener_depth as usize,
105            )),
106        })
107        .collect()
108}
109
110/// Markdown heading-based fold provider (DESIGN.md §15:18,
111/// `docs/user/folding.md`). Walks the buffer for ATX headings
112/// (`^#+\s`) and emits one fold per heading whose body has at
113/// least one row. Heading depth (the number of `#`s) determines
114/// nesting: a `## H2` ends at the next same-or-shallower heading
115/// (`# H1` or another `## H2` or end-of-buffer).
116///
117/// Code-fence aware: `^```` lines toggle a "in fenced block" state;
118/// `#`-prefixed lines inside a fenced block are not headings. The
119/// fence itself is included in whichever fold contains it.
120///
121/// Lines inside fenced code blocks of the form `~~~` are also
122/// excluded from heading detection, mirroring CommonMark §4.5.
123pub fn compute_markdown_folds(buffer: &Buffer) -> Vec<Fold> {
124    let text = buffer.as_string();
125    let lines: Vec<&str> = text.split('\n').collect();
126    // `split('\n')` on a string ending in `\n` produces a trailing
127    // empty element; that empty element isn't an addressable buffer
128    // line and a fold extending to it would over-delete in the
129    // fold-aware operator path. Use the last addressable index
130    // instead.
131    let last_addressable = if lines.last().is_some_and(|s| s.is_empty()) && lines.len() > 1 {
132        lines.len() - 2
133    } else {
134        lines.len().saturating_sub(1)
135    };
136    if last_addressable == 0 {
137        return Vec::new();
138    }
139
140    // First pass: find every heading line and its depth, skipping
141    // those inside fenced code blocks.
142    let mut headings: Vec<(usize, u32)> = Vec::new(); // (line_idx, depth)
143    let mut in_fence = false;
144    for (i, line) in lines.iter().enumerate() {
145        let trimmed = line.trim_start();
146        if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
147            in_fence = !in_fence;
148            continue;
149        }
150        if in_fence {
151            continue;
152        }
153        if let Some(depth) = atx_heading_depth(line) {
154            headings.push((i, depth));
155        }
156    }
157    if headings.is_empty() {
158        return Vec::new();
159    }
160
161    // Second pass: each heading folds from its row to the row
162    // before the next same-or-shallower heading, or end-of-buffer.
163    // Single-line "headings" (next heading on the very next row)
164    // are skipped because the body would be empty.
165    let mut folds: Vec<Fold> = Vec::new();
166    for (h_idx, &(start, depth)) in headings.iter().enumerate() {
167        let end = headings
168            .iter()
169            .skip(h_idx + 1)
170            .find(|(_, d)| *d <= depth)
171            .map(|(line_idx, _)| line_idx.saturating_sub(1))
172            .unwrap_or(last_addressable);
173        if end <= start {
174            // Empty body (next sibling heading immediately follows).
175            continue;
176        }
177        // Identity uses the heading line + depth so headings with
178        // identical text at different depths (`# Foo` vs `## Foo`)
179        // get different identities.
180        let identity = fold_identity(lines[start], depth as usize);
181        folds.push(Fold {
182            start_line: start as u32,
183            end_line: end as u32,
184            closed: false,
185            identity: Some(identity),
186        });
187    }
188    folds
189}
190
191/// Recognise an ATX heading line (`#` to `######` followed by at
192/// least one whitespace) per CommonMark §4.2. Returns the heading
193/// depth (1-6) on match, `None` otherwise. Leading whitespace is
194/// allowed but capped at 3 spaces (CommonMark's "up to 3 leading
195/// spaces" rule); 4+ leading spaces means the line is part of an
196/// indented code block.
197fn atx_heading_depth(line: &str) -> Option<u32> {
198    let lead = line.chars().take_while(|c| *c == ' ').count();
199    if lead > 3 {
200        return None;
201    }
202    let rest = &line[lead..];
203    let hashes = rest.chars().take_while(|c| *c == '#').count() as u32;
204    if !(1..=6).contains(&hashes) {
205        return None;
206    }
207    let after = &rest[hashes as usize..];
208    // CommonMark requires at least one whitespace between hashes
209    // and content (or the line ends, e.g. `#` alone is a valid
210    // heading with empty content).
211    if after.is_empty() || after.starts_with([' ', '\t']) {
212        Some(hashes)
213    } else {
214        None
215    }
216}
217
218/// Tree-sitter-driven fold provider. Runs the language's compiled
219/// `folds.scm` query against [`lattice_syntax::Syntax`]'s parse tree
220/// and emits one [`Fold`] per `@fold` capture spanning more than a
221/// single line.
222///
223/// Returns `None` when:
224/// - The buffer's language doesn't ship a `folds.scm` (e.g. plain
225///   text or the inline-markdown parser). The caller cascades to the
226///   markdown / indent providers.
227/// - The syntax tree isn't available yet (first parse hasn't run).
228///
229/// Returns `Some(Vec::new())` when the language has a query but the
230/// document genuinely has no foldable structures (an empty file, a
231/// single-line file). The empty Vec lets the caller know "syntax
232/// authoritative, just nothing to fold" so it doesn't fall through
233/// to indent.
234pub fn compute_syntax_folds(syntax: &SyntaxSnapshot) -> Option<Vec<Fold>> {
235    let tree = syntax.tree()?;
236    let source = syntax.source();
237    let registry = syntax.registry();
238    let query = registry.folds_query(syntax.lang().name())?;
239    // Only `@fold` captures fold. The folds query may ALSO carry `@codeblock`
240    // captures — read by `compute_code_block_lines` for the code-block tint —
241    // and those must not become folds. Every in-repo grammar uses `@fold`, so
242    // this is a no-op for them; it exists so the two concerns can share one
243    // query without a `@codeblock`-only region silently folding. A query with
244    // no `@fold` capture folds nothing, which is the correct answer for it.
245    let fold_cap = query.capture_index_for_name("fold");
246
247    let mut cursor = QueryCursor::new();
248    let mut matches = cursor.matches(query, tree.root_node(), source);
249
250    // Precompute line-start byte offsets in ONE pass so the per-fold
251    // start-line-text lookup below is O(1). The old code called
252    // `line_at(source, start_line)`, which rescans from byte 0 every call;
253    // with one call per fold that is O(folds × file_size) = O(n²) — ~34 s on
254    // a 36k-line file (dispatch.rs), the freeze this fixes. `line_starts[i]`
255    // is the byte offset where line `i` begins.
256    let line_starts: Vec<usize> = std::iter::once(0)
257        .chain(
258            source
259                .iter()
260                .enumerate()
261                .filter_map(|(i, &b)| (b == b'\n').then_some(i + 1)),
262        )
263        .collect();
264    let line_text = |line: u32| -> &str {
265        let li = line as usize;
266        let Some(&start) = line_starts.get(li) else {
267            return "";
268        };
269        // Content ends just before the next line's start (the `\n`), or at
270        // EOF for the last line.
271        let end = line_starts
272            .get(li + 1)
273            .map(|&next| next.saturating_sub(1))
274            .unwrap_or(source.len());
275        std::str::from_utf8(source.get(start..end).unwrap_or(&[])).unwrap_or("")
276    };
277
278    let mut folds: Vec<Fold> = Vec::new();
279    let mut seen: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
280
281    while let Some(m) = matches.next() {
282        for cap in m.captures {
283            // Ignore non-`@fold` captures (e.g. `@codeblock`) sharing this query.
284            if Some(cap.index) != fold_cap {
285                continue;
286            }
287            let node = cap.node;
288            let start_line = node.start_position().row as u32;
289            let end_line = node.end_position().row as u32;
290            // Tree-sitter's `end_position` lands on the line *after*
291            // the node's last content character when the node ends
292            // with a newline (e.g. block_comment, fenced_code_block).
293            // Pull back one line so the user-facing fold range
294            // corresponds to lines that actually contain the node's
295            // content.
296            let end_line = if end_line > start_line && node.end_byte() > 0 {
297                let last_byte = node.end_byte().saturating_sub(1);
298                if source.get(last_byte) == Some(&b'\n') {
299                    end_line.saturating_sub(1)
300                } else {
301                    end_line
302                }
303            } else {
304                end_line
305            };
306            // Skip single-line captures: nothing to hide.
307            if end_line <= start_line {
308                continue;
309            }
310            // De-dup: tree-sitter can emit the same node range under
311            // multiple patterns; folds are keyed on byte range so
312            // duplicates would just bloat the vec.
313            if !seen.insert((start_line, end_line)) {
314                continue;
315            }
316            let start_line_text = line_text(start_line);
317            let identity = syntax_fold_identity(node.kind(), start_line_text);
318            folds.push(Fold {
319                start_line,
320                end_line,
321                closed: false,
322                identity: Some(identity),
323            });
324        }
325    }
326    folds.sort_by(|a, b| {
327        a.start_line
328            .cmp(&b.start_line)
329            .then_with(|| b.end_line.cmp(&a.end_line))
330    });
331    Some(folds)
332}
333
334/// MC.2: the source lines that lie inside a fenced (or indented) code block,
335/// for the full-width `syntax.code_block` background tint (markdown and any
336/// grammar exposing those node kinds).
337///
338/// Mirrors [`compute_syntax_folds`]: it runs the language's fold query — the one
339/// registered query that already captures `fenced_code_block` /
340/// `indented_code_block` — and keeps only the code-block captures, expanding
341/// each to the individual source lines it covers. A grammar whose fold query
342/// captures no code block (every language but markdown today) yields an empty
343/// set, so no tint. Runs on the actor thread off the paint path, exactly like
344/// the fold recompute it rides beside.
345///
346/// Returned lines are sorted and de-duplicated (a `BTreeSet` collapses the
347/// overlap tree-sitter can report for the same range under multiple patterns).
348pub fn compute_code_block_lines(syntax: &SyntaxSnapshot) -> Option<Vec<u32>> {
349    let tree = syntax.tree()?;
350    let source = syntax.source();
351    let registry = syntax.registry();
352    let query = registry.folds_query(syntax.lang().name())?;
353    // A grammar declares its code-block-background regions with a `@codeblock`
354    // capture in its folds query; the host tints whatever is captured, so it
355    // knows no language-specific node kinds. Markdown captures its fenced /
356    // indented blocks; a plugin grammar (e.g. org) captures its own src blocks
357    // the same way, reusing the `syntax.code_block` theme element. No
358    // `@codeblock` capture ⇒ this language has no code-block tint.
359    let codeblock_cap = query.capture_index_for_name("codeblock")?;
360
361    let mut cursor = QueryCursor::new();
362    let mut matches = cursor.matches(query, tree.root_node(), source);
363
364    let mut lines: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
365    while let Some(m) = matches.next() {
366        for cap in m.captures {
367            if cap.index != codeblock_cap {
368                continue;
369            }
370            let node = cap.node;
371            let start_line = node.start_position().row as u32;
372            let mut end_line = node.end_position().row as u32;
373            // Same trailing-newline pullback as `compute_syntax_folds`: a node
374            // ending in '\n' reports `end_position` on the following line, which
375            // would tint one blank row past the block.
376            if end_line > start_line && node.end_byte() > 0 {
377                let last_byte = node.end_byte().saturating_sub(1);
378                if source.get(last_byte) == Some(&b'\n') {
379                    end_line = end_line.saturating_sub(1);
380                }
381            }
382            for line in start_line..=end_line {
383                lines.insert(line);
384            }
385        }
386    }
387    Some(lines.into_iter().collect())
388}
389
390/// Hash the user-visible signature of the current fold set.
391///
392/// Used as part of the highlights cache key
393/// ([`crate::render_state::VisibleHighlightsKey::fold_hash`]) so a
394/// fold toggle invalidates cached spans: collapsing / expanding a
395/// fold changes which physical lines are visible, which changes
396/// what `highlight_lines(start, end)` should produce.
397///
398/// Only `(start_line, end_line, closed)` are hashed —
399/// fold `identity` is excluded because two folds with the same
400/// range and state but different identities don't change which
401/// bytes are visible.
402///
403// ── FL.1: fold levels ─────────────────────────────────────────
404
405/// Nesting level of each fold, 1-based, in `folds` order.
406///
407/// Vim's model: the outermost fold is level 1, and `foldlevel=N`
408/// closes everything deeper than `N` (so `foldlevel=0` closes
409/// everything). A fold's level is one more than the number of folds
410/// that **properly** contain it.
411///
412/// *Properly* is the load-bearing word. Two providers routinely emit
413/// folds over the identical range — a multibuffer file that contributes
414/// exactly one excerpt gets a `FileBoundaryFoldProvider` fold and an
415/// `ExcerptFoldProvider` fold with the same bounds. Under a
416/// `start <= start && end >= end` test each would "contain" the other,
417/// both would land at level 2, and `foldlevel=1` would collapse a view
418/// that has only one level of structure to show. Equal ranges are
419/// siblings, not parent and child.
420///
421/// O(n²) in the fold count — the same shape as the closed-state
422/// carry-over `recompute_folds` already runs over the same list, so this
423/// adds a constant factor rather than a complexity class. Both callers
424/// skip it entirely at the default `foldlevel` (see
425/// [`level_opens_everything`]), which is the case that runs after every
426/// reparse.
427pub fn fold_levels(folds: &[Fold]) -> Vec<u32> {
428    folds
429        .iter()
430        .map(|f| {
431            1 + folds
432                .iter()
433                .filter(|o| {
434                    o.start_line <= f.start_line
435                        && o.end_line >= f.end_line
436                        && (o.start_line < f.start_line || o.end_line > f.end_line)
437                })
438                .count() as u32
439        })
440        .collect()
441}
442
443/// Close every fold deeper than `level`, open the rest.
444///
445/// This is what `:set foldlevel=N` does at the moment it is set. It is
446/// deliberately a one-shot bulk action rather than a standing invariant:
447/// a user who then presses `za` on one fold must not have it reopened by
448/// the next rebuild, so `recompute_folds` carries manual state over by
449/// identity and consults `foldlevel` only for folds it has not seen
450/// before (see [`apply_fold_level_to_new`]).
451pub fn apply_fold_level(folds: &mut [Fold], level: u32) {
452    if level_opens_everything(folds, level) {
453        for fold in folds.iter_mut() {
454            fold.closed = false;
455        }
456        return;
457    }
458    let levels = fold_levels(folds);
459    for (fold, depth) in folds.iter_mut().zip(levels) {
460        fold.closed = depth > level;
461    }
462}
463
464/// Can any fold in `folds` be deeper than `level`?
465///
466/// A fold's level is bounded by the number of folds, so `level >= len`
467/// answers "no" without computing anything. This is the default case —
468/// `foldlevel` ships at 99 — and it is the one that runs after every
469/// reparse, so the quadratic level pass never touches the path a user
470/// is typing on.
471fn level_opens_everything(folds: &[Fold], level: u32) -> bool {
472    level as usize >= folds.len()
473}
474
475/// Why [`crate::editor::Editor::recompute_folds_because`] is running, and so
476/// whether `foldlevel` gets to seed the folds it has not seen before.
477///
478/// `fold-architecture.md` states that `foldlevel` "applies as a bulk action,
479/// not a standing invariant" — [`apply_fold_level_to_new`] is the narrow
480/// exception that lets it decide the initial state of structure that shows up
481/// later. On the edit path that exception had grown back into the invariant it
482/// was carved out of: every keystroke that created a fold handed `foldlevel` a
483/// fold it had never seen, and at org's `foldlevel=0` that fold closed under
484/// the cursor. `o` on a bare headline was the purest case — a headline with no
485/// body produces no fold at all, so opening a line under it makes one appear
486/// for the very first time, mid-insert.
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum FoldRecomputeCause {
489    /// Content arrived from somewhere other than the user's hands: buffer
490    /// activation, a late syntax attach, an async provider batch, an LSP
491    /// folding-range response. This is structure the user has genuinely not
492    /// seen, so `foldlevel` seeds it — org opens on an overview, `:set
493    /// foldlevel=0` keeps collapsing a project diff whose hunks are still
494    /// streaming in.
495    Populate,
496    /// The user's own edit reshaped the buffer. `foldlevel` gets no vote: a
497    /// fold that exists because a keystroke just created it is not structure
498    /// the user has yet to see — they are looking at it, and usually typing
499    /// inside it. Emacs (`#+STARTUP: overview`) and vim (`foldlevelstart`)
500    /// both treat level-driven collapse as an opening act for the same reason.
501    Edit,
502}
503
504/// Close new folds that sit deeper than `foldlevel`, leaving folds
505/// whose state was carried over untouched.
506///
507
508/// `carried[i]` is true when `folds[i]` inherited its `closed` flag from
509/// a previous fold with the same identity. Those keep whatever the user
510/// last did to them; the rest are new structure, and new structure obeys
511/// the option.
512///
513/// **Only ever closes.** A provider may emit a fold already closed —
514/// diff-mode's unchanged-region folds are the in-tree case, and the
515/// whole point of them is that the unchanged stretches start collapsed.
516/// Assigning `closed = depth > level` here rather than OR-ing reopened
517/// every one of them at the default level, because a level-1 fold is not
518/// deeper than 99. `foldlevel` decides whether nesting depth *adds* a
519/// close; it does not get a vote on a provider's own intent. Reopening
520/// is the bulk [`apply_fold_level`] path, which is a user action.
521pub fn apply_fold_level_to_new(folds: &mut [Fold], carried: &[bool], level: u32) {
522    if level_opens_everything(folds, level) {
523        return;
524    }
525    let levels = fold_levels(folds);
526    for ((fold, depth), carried) in folds.iter_mut().zip(levels).zip(carried) {
527        if !*carried {
528            fold.closed |= depth > level;
529        }
530    }
531}
532
533/// Deepest level present, or 0 when there are no folds. `zR`'s
534/// equivalent `foldlevel` value.
535pub fn max_fold_level(folds: &[Fold]) -> u32 {
536    fold_levels(folds).into_iter().max().unwrap_or(0)
537}
538
539/// Phase 5.8.AF.5 / Slice X2: hoisted host-side from
540/// `lattice-ui-tui::app::folds::compute_fold_hash` so dispatch's
541/// `publish_render_state` can populate
542/// `SyntaxRenderState::fold_hash` without depending on the
543/// renderer crate.
544pub fn compute_fold_hash(folds: &[Fold]) -> u64 {
545    use std::hash::{DefaultHasher, Hash, Hasher};
546    let mut h = DefaultHasher::new();
547    folds.len().hash(&mut h);
548    for f in folds {
549        f.start_line.hash(&mut h);
550        f.end_line.hash(&mut h);
551        f.closed.hash(&mut h);
552    }
553    h.finish()
554}
555
556/// VM.3i: vim's `zj` / `zk` edge from `line` — the nearest fold START after
557/// it (`forward`) or fold END before it — among edges that are VISIBLE.
558///
559/// "A closed fold is counted as one fold": folds nested inside a closed one
560/// render on its row, so they aren't stops. Candidates are compared by the row
561/// they're displayed on ([`FoldIndex::visible_anchor`]), and an edge on the
562/// cursor's own row isn't a destination. The edges returned are still line
563/// numbers, so `zk` from below a collapsed section lands on its last line.
564///
565/// One rule for both callers: the `zj` / `zk` motions (through the host's
566/// fold resolver) and `Editor::do_goto_fold` (the WIT action path).
567pub fn visible_fold_edge(
568    folds: &[Fold],
569    foldenable: bool,
570    line: u32,
571    forward: bool,
572) -> Option<u32> {
573    let idx = FoldIndex::from_folds(folds, foldenable);
574    let here = idx.visible_anchor(line);
575    if forward {
576        folds
577            .iter()
578            .map(|f| f.start_line)
579            .filter(|&s| s > line && idx.visible_anchor(s) > here)
580            .min()
581    } else {
582        folds
583            .iter()
584            .map(|f| f.end_line)
585            .filter(|&e| e < line && idx.visible_anchor(e) < here)
586            .max()
587    }
588}
589
590/// O(log folds) lookup index built once per frame (or per publish) over
591/// a snapshot of the active document's folds.
592///
593/// Perf plan C. Renderers used to call
594/// `folds.iter().any(|f| ...)` per visible line in the gutter compose
595/// loop — `O(rows × folds)` per pane per frame. With this index the
596/// per-line check drops to a partition-point binary search plus a
597/// constant-time fast path for the common non-overlapping case.
598///
599/// Build cost is `O(folds)`; for the typical buffer with <50 folds
600/// it's <1 µs. The build is intentionally per-frame on the renderer
601/// side (no host-side caching / invalidation discipline) — the saving
602/// is the per-line walk, not the construction.
603///
604/// Fold semantics (paramount goal #3, vim parity):
605/// - `closed_fold_start_at(line)` is true iff `line == f.start_line`
606///   for some closed fold `f`.
607/// - `line_inside_closed_fold(line)` is true iff
608///   `f.start_line < line && line <= f.end_line` for some closed fold.
609///   Start lines are NOT inside (vim renders the heading row).
610/// - `fold_start_at_any(line)` covers open + closed folds. Used by
611///   the gutter glyph provider (open-fold caret vs closed-fold chevron).
612#[derive(Debug, Clone, Default)]
613pub struct FoldIndex {
614    /// Closed-fold `(start_line, end_line)` pairs sorted by `start_line`.
615    /// Two `u32`s fit in 8 bytes — cache-friendly linear / binary walks.
616    closed: Vec<(u32, u32)>,
617    /// All-fold `start_line`s (open + closed) sorted ascending.
618    all_starts: Vec<u32>,
619    /// `:set foldenable` cached at build time so every predicate is a
620    /// single bool branch ahead of any vec walk; the user-facing
621    /// behaviour of all three predicates collapses to `false` when
622    /// foldenable is off, exactly matching the existing `Editor::*`
623    /// helpers in `dispatch.rs`.
624    foldenable: bool,
625}
626
627impl FoldIndex {
628    /// Build an index from a fold snapshot. `O(folds)` — one filter +
629    /// two sorts.
630    pub fn from_folds(folds: &[Fold], foldenable: bool) -> Self {
631        let mut closed: Vec<(u32, u32)> = folds
632            .iter()
633            .filter(|f| f.closed)
634            .map(|f| (f.start_line, f.end_line))
635            .collect();
636        closed.sort_unstable_by_key(|(s, _)| *s);
637        let mut all_starts: Vec<u32> = folds.iter().map(|f| f.start_line).collect();
638        all_starts.sort_unstable();
639        Self {
640            closed,
641            all_starts,
642            foldenable,
643        }
644    }
645
646    /// True iff `line` is the start row of a closed fold. Matches
647    /// `Editor::fold_start_at(line).is_some()` semantics.
648    pub fn closed_fold_start_at(&self, line: u32) -> bool {
649        if !self.foldenable {
650            return false;
651        }
652        self.closed.binary_search_by_key(&line, |(s, _)| *s).is_ok()
653    }
654
655    /// `(start, end)` of the closed fold starting at `line`, or
656    /// `None` if no closed fold starts there. Mirrors
657    /// `Editor::fold_start_at(line)` but yields just the row range
658    /// the renderer + fold-aware viewport math actually need —
659    /// `&Fold` would force us to hold a `Vec<Fold>` snapshot in the
660    /// index, while two `u32`s are 8 bytes per entry and cache-
661    /// friendly. Used by `Editor::fold_aware_highlight_end_line`
662    /// to advance `buf_line` past a closed fold in the viewport
663    /// stretch loop.
664    pub fn closed_fold_at(&self, line: u32) -> Option<(u32, u32)> {
665        if !self.foldenable {
666            return None;
667        }
668        self.closed
669            .binary_search_by_key(&line, |(s, _)| *s)
670            .ok()
671            .map(|i| self.closed[i])
672    }
673
674    /// The innermost closed fold whose *interior* contains `line`
675    /// (`start_line < line <= end_line`), or `None`. The interior
676    /// excludes the start row — the fold head stays visible — so a
677    /// head line reports `None` here even though it bounds a fold.
678    ///
679    /// Used by the fold-aware scroll walk
680    /// (`Editor::bottom_anchored_scroll`) to hop from a hidden body
681    /// line straight up to its visible head in one step, instead of
682    /// iterating every collapsed line. `line_inside_closed_fold`
683    /// answers the same question as a bool; this returns the range
684    /// so the caller can jump.
685    pub fn enclosing_closed_fold(&self, line: u32) -> Option<(u32, u32)> {
686        if !self.foldenable {
687            return None;
688        }
689        // `closed` is sorted by start_line ascending. The rightmost
690        // entry with `start < line` is the innermost candidate; for
691        // non-overlapping / properly-nested folds (the common case)
692        // it is also the only one that can enclose `line`.
693        let idx = self.closed.partition_point(|(s, _)| *s < line);
694        if idx == 0 {
695            return None;
696        }
697        let (s, e) = self.closed[idx - 1];
698        if e >= line {
699            return Some((s, e));
700        }
701        // Slow path: only reachable when folds overlap (rare — e.g.
702        // user manually `:fold`s overlapping ranges). Walk left for
703        // the nearest encloser.
704        self.closed[..idx - 1]
705            .iter()
706            .rev()
707            .find(|(_, e)| *e >= line)
708            .copied()
709    }
710
711    /// The row `line` is actually DISPLAYED on: itself when visible, else
712    /// the head of the outermost closed fold that swallows it.
713    ///
714    /// Two lines share an anchor exactly when they occupy the same row on
715    /// screen. That is what makes it the right comparison for a motion that
716    /// has to *look* like it moved — `zj` / `zk` step between fold edges, and
717    /// an edge collapsed inside a closed fold is not a place the cursor can
718    /// visibly go (see `Editor::do_goto_fold`).
719    ///
720    /// **Outermost, not innermost**, which is why this walks rather than
721    /// calling [`Self::enclosing_closed_fold`] once: closed folds nest, and a
722    /// sub-fold's head is itself hidden when its parent is also closed. The
723    /// walk climbs head-to-head until it reaches one nothing encloses, so the
724    /// answer is a row that is genuinely on screen. Bounded by fold nesting
725    /// depth (single digits in practice) and each step is the same binary
726    /// search the predicates beside it use.
727    ///
728    /// Collapses to the identity under `:set nofoldenable`, like every other
729    /// predicate here — nothing is folded, so nothing is hidden.
730    pub fn visible_anchor(&self, line: u32) -> u32 {
731        if !self.foldenable {
732            return line;
733        }
734        let mut at = line;
735        // `closed` bounds the climb: each step moves strictly left (a fold's
736        // head is below the line it encloses), so this cannot loop even if a
737        // user hand-folds overlapping ranges.
738        for _ in 0..self.closed.len() {
739            match self.enclosing_closed_fold(at) {
740                Some((start, _)) => at = start,
741                None => break,
742            }
743        }
744        at
745    }
746
747    /// True iff `line` falls strictly inside the interior of some closed
748    /// fold (`start_line < line <= end_line`). Matches the existing
749    /// `Editor::line_inside_closed_fold` semantics.
750    pub fn line_inside_closed_fold(&self, line: u32) -> bool {
751        if !self.foldenable {
752            return false;
753        }
754        // `closed` is sorted by start_line ascending. The rightmost
755        // entry with `start < line` is the most-recently-opened
756        // candidate; for non-overlapping folds (the common case) it
757        // is also the only candidate that can enclose `line`.
758        let idx = self.closed.partition_point(|(s, _)| *s < line);
759        if idx == 0 {
760            return false;
761        }
762        // Fast path: innermost (or only) candidate. Constant time.
763        if self.closed[idx - 1].1 >= line {
764            return true;
765        }
766        // Slow path: only reachable when folds overlap (rare —
767        // e.g. user manually `:fold`s overlapping ranges). Bounded
768        // by `idx`, but in practice walks a tiny prefix.
769        self.closed[..idx - 1].iter().any(|(_, e)| line <= *e)
770    }
771
772    /// True iff `line` is the start row of any fold (open or closed).
773    /// Mirrors `Editor::fold_start_at_any(line).is_some()`.
774    pub fn fold_start_at_any(&self, line: u32) -> bool {
775        if !self.foldenable {
776            return false;
777        }
778        self.all_starts.binary_search(&line).is_ok()
779    }
780
781    /// Whether a fold *starts* at `line`, and if so whether it is
782    /// collapsed. `Some(true)` = closed fold head, `Some(false)` = open
783    /// (expanded) fold head, `None` = no fold starts here. Gates on
784    /// `foldenable`. This is what a gutter renderer consults to pick the
785    /// collapsed vs expanded marker glyph — the shared peer of the TUI's
786    /// `fold_glyph_for`, so both renderers show a marker on every
787    /// foldable head (not just collapsed ones) and agree on which glyph.
788    pub fn fold_start_kind_at(&self, line: u32) -> Option<FoldMarker> {
789        if !self.foldenable || self.all_starts.binary_search(&line).is_err() {
790            return None;
791        }
792        if self.closed.binary_search_by_key(&line, |(s, _)| *s).is_ok() {
793            Some(FoldMarker::Closed)
794        } else {
795            Some(FoldMarker::Open)
796        }
797    }
798}
799
800/// A gutter fold marker's state: the head row of an open (expanded) fold
801/// versus a closed (collapsed) one. Renderers map this to their glyph +
802/// themed colour (`gutter.fold.open` / `gutter.fold.closed`).
803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804pub enum FoldMarker {
805    /// Expanded fold — body visible below the head. Rendered `▾`.
806    Open,
807    /// Collapsed fold — body hidden onto the head. Rendered `▸`.
808    Closed,
809}
810
811/// Number of buffer lines collapsed onto the visible head row of the
812/// closed fold `(start_line, end_line)` — the count both renderers show
813/// in the `⋯ N lines` fold summary. Walks forward from `end_line + 1`
814/// through any sibling closed folds whose *heading is itself hidden* by
815/// the region already collapsed (`start < probe <= end`), so overlapping
816/// folds (e.g. `(1,3)+(3,5)` from `foldmethod=indent`) report their
817/// combined span. Folds that merely ABUT — the next fold starts at
818/// `end + 1`, the first *visible* line after this one — are NOT chained:
819/// that fold has an on-screen heading and is a separate fold with its
820/// own summary. Shared by the TUI and GPUI renderers so the count stays
821/// identical.
822/// The source lines a pane actually shows, in paint order: walk from
823/// `scroll` collecting lines until `height` VISIBLE ones are gathered,
824/// skipping every line hidden inside a closed fold and stepping over a
825/// closed fold's body in one jump (its head row stands for the whole
826/// range).
827///
828/// The bound is **display rows, not source lines**, and that is the
829/// entire point. Taking `[scroll, scroll + height)` and filtering the
830/// folded lines out afterwards looks equivalent and is not: each
831/// collapsed line still spends one of the window's slots, so the pane
832/// under-fills the moment a fold closes and every line past the window
833/// — further headings, the cursor — is never considered at all. GPUI
834/// shipped that bug (2026-08-09); the TUI had this walk inline from the
835/// start. It lives here now so the two peers cannot drift again.
836///
837/// Soft-wrap is deliberately NOT accounted for: a wrapped line's extra
838/// segments are display rows the renderers add downstream, so this can
839/// over-collect when wrapping is on. Both renderers cap their row
840/// budget while emitting (`shaped_text.len() >= viewport_height` /
841/// `out.len() >= height`), which is where wrap is paid for.
842///
843/// **`total_lines` is CONTENT space** —
844/// [`lattice_core::Buffer::content_line_count`], never ropey's raw
845/// [`lattice_core::Buffer::line_count`]. This function decides which
846/// source lines get a row, so the raw count's phantom trailing line
847/// (present for every rope ending in `\n`, i.e. every normal file)
848/// becomes a phantom painted row: the numbered blank line 220 of a
849/// 219-line file that CV.2 was reported for. Both renderers call
850/// through here, so passing the wrong space in either one reintroduces
851/// it in that peer alone.
852pub fn visible_source_lines(
853    fold_index: &FoldIndex,
854    scroll: u32,
855    height: u32,
856    total_lines: u32,
857) -> Vec<u32> {
858    let mut out: Vec<u32> = Vec::with_capacity(height as usize);
859    let mut li = scroll.min(total_lines);
860    while (out.len() as u32) < height && li < total_lines {
861        if fold_index.line_inside_closed_fold(li) {
862            li += 1;
863            continue;
864        }
865        out.push(li);
866        li = match fold_index.closed_fold_at(li) {
867            Some((_, end)) => end.saturating_add(1),
868            None => li + 1,
869        };
870    }
871    out
872}
873
874/// The exclusive buffer-line bound a `height`-row viewport starting at
875/// `scroll` actually reaches.
876///
877/// `height` counts **display rows**; the answer counts **buffer lines**,
878/// and a closed fold is exactly where those two stop being the same
879/// number. Collapse an org file and 60 rows can reach 2500 lines in.
880/// Anything sized to the band a viewport covers — the syntax parse
881/// window, the cell matrix's chunk window — must ask for this rather
882/// than `scroll + height`, or it stops one screenful of *lines* down
883/// while the screen keeps painting past it.
884///
885/// A viewport ending on a closed fold head returns the bound past that
886/// fold's whole body. The body has no rows, so this only ever makes a
887/// caller's range a superset of what it strictly needs; it also means
888/// consecutive callers agree with the head row's own extent.
889///
890/// `total_lines` is CONTENT space, for the reason spelled out on
891/// [`visible_source_lines`].
892pub fn fold_aware_visible_end(
893    fold_idx: &FoldIndex,
894    scroll: u32,
895    height: u32,
896    total_lines: u32,
897) -> u32 {
898    let plain = scroll.saturating_add(height).min(total_lines);
899    if !fold_idx.foldenable || height == 0 || total_lines == 0 {
900        return plain;
901    }
902    let mut buf_line = scroll;
903    let mut row = 0u32;
904    let mut last = scroll;
905    while row < height && buf_line < total_lines {
906        if fold_idx.line_inside_closed_fold(buf_line) {
907            // Defensive: `scroll` should always sit on a visible line.
908            last = buf_line;
909            buf_line += 1;
910            continue;
911        }
912        last = buf_line;
913        match fold_idx.closed_fold_at(buf_line) {
914            Some((_, end)) => {
915                last = end;
916                buf_line = end.saturating_add(1);
917            }
918            None => buf_line += 1,
919        }
920        row += 1;
921    }
922    // `last` is the highest buffer line inside the visible band; the
923    // bound is exclusive. Never SHRINK below the fold-free answer: a
924    // viewport that runs past EOF still asks for the same range it
925    // always did, so a fold-free buffer is bit-identical to before.
926    last.saturating_add(1).min(total_lines).max(plain)
927}
928
929/// [`fold_aware_visible_end`] from a raw fold slice, skipping the
930/// `FoldIndex` build when there is no closed fold to walk over — the
931/// overwhelmingly common case, and one that runs per worker tick.
932pub fn fold_aware_visible_end_of(
933    folds: &[Fold],
934    foldenable: bool,
935    scroll: u32,
936    height: u32,
937    total_lines: u32,
938) -> u32 {
939    if !foldenable || height == 0 || total_lines == 0 || !folds.iter().any(|f| f.closed) {
940        return scroll.saturating_add(height).min(total_lines);
941    }
942    let idx = FoldIndex::from_folds(folds, foldenable);
943    fold_aware_visible_end(&idx, scroll, height, total_lines)
944}
945
946/// The ` ⋯ N lines` summary text trailing a collapsed head row, where
947/// `n` is [`folded_line_span`]'s count. One function so the TUI and
948/// GPUI peers cannot drift on spacing, glyph, or pluralisation — the
949/// TUI grew this as an inline `format!` and GPUI had no summary at
950/// all, which is exactly how two renderers end up disagreeing.
951///
952/// Callers paint it as trailing DECORATION: it sits outside the source
953/// column axis, so it must never widen the row's wrap-segment count
954/// (see `docs/dev/architecture/fold-architecture.md` §5).
955pub fn fold_summary_text(n: u32) -> String {
956    format!(" ⋯ {n} lines")
957}
958
959pub fn folded_line_span(folds: &[Fold], start_line: u32, end_line: u32, total_lines: u32) -> u32 {
960    let mut end = end_line;
961    let mut probe = end.saturating_add(1);
962    while probe < total_lines {
963        let next = folds
964            .iter()
965            .find(|f| f.closed && probe > f.start_line && probe <= f.end_line);
966        match next {
967            Some(f) => {
968                end = end.max(f.end_line);
969                probe = end.saturating_add(1);
970            }
971            None => break,
972        }
973    }
974    end.saturating_sub(start_line).saturating_add(1)
975}
976
977/// Walk forward from `start_line` and return the buffer line at
978/// `offset` visible (non-fold-hidden) display rows below it.
979/// Closed fold bodies are skipped — only fold heads (start lines)
980/// and non-folded lines count as visible rows. When the walk
981/// reaches the last addressable line before consuming `offset`
982/// rows, that last line is returned (the caller's clamp).
983pub fn nth_visible_line_forward(
984    fold_idx: &FoldIndex,
985    start_line: u32,
986    offset: u32,
987    total_lines: u32,
988) -> u32 {
989    if offset == 0 || !fold_idx.foldenable {
990        return start_line.min(total_lines.saturating_sub(1));
991    }
992    let last = total_lines.saturating_sub(1);
993    let mut line = start_line;
994    let mut count = 0u32;
995    while count < offset && line < last {
996        line += 1;
997        while let Some((_, end)) = fold_idx.enclosing_closed_fold(line) {
998            line = end + 1;
999            if line > last {
1000                return last;
1001            }
1002        }
1003        count += 1;
1004    }
1005    line.min(last)
1006}
1007
1008/// Walk backward from `start_line` and return the line at `offset`
1009/// visible rows above it. Walking backward hops over closed fold
1010/// bodies to their visible heads, then continues from the preceding
1011/// line. Returns 0 when the walk hits BOF before consuming `offset`
1012/// rows.
1013pub fn nth_visible_line_backward(fold_idx: &FoldIndex, start_line: u32, offset: u32) -> u32 {
1014    if offset == 0 || !fold_idx.foldenable {
1015        return start_line;
1016    }
1017    let mut line = start_line;
1018    let mut count = 0u32;
1019    while count < offset && line > 0 {
1020        line = line.saturating_sub(1);
1021        while let Some((start, _)) = fold_idx.enclosing_closed_fold(line) {
1022            line = start;
1023        }
1024        count += 1;
1025    }
1026    line
1027}
1028
1029/// Count how many visible (non-fold-hidden) rows exist between
1030/// `from_line` (inclusive) and `to_line` (exclusive). Closed fold
1031/// bodies are skipped. Both lines must be *visible* (not inside a
1032/// closed fold body) — callers ensure this before calling. Returns
1033/// the number of display rows spanning the range.
1034pub fn count_visible_rows_between(fold_idx: &FoldIndex, from_line: u32, to_line: u32) -> u32 {
1035    if from_line >= to_line || !fold_idx.foldenable {
1036        return to_line.saturating_sub(from_line);
1037    }
1038    let mut line = from_line;
1039    let mut count = 0u32;
1040    while line < to_line {
1041        count += 1;
1042        if let Some((_, end)) = fold_idx.closed_fold_at(line) {
1043            line = end + 1;
1044        } else {
1045            line += 1;
1046        }
1047    }
1048    count
1049}
1050
1051// =========================================================
1052// D.3.f.0 (2026-05-29): primary-provider impls wrapping the
1053// existing `compute_*_folds` helpers. See
1054// `docs/dev/architecture/fold-architecture.md`. These wire
1055// into the `FoldRegistry` constructed by `Editor::boot`.
1056// Behaviour is unchanged from the pre-refactor
1057// `Editor::recompute_folds` match arms — the registry
1058// dispatch produces the same fold sets for the same inputs.
1059// =========================================================
1060
1061use crate::fold_provider::{FoldContext, FoldProvider};
1062use lattice_core::{ProviderId, ProviderKind};
1063
1064const PROVIDER_ID_MANUAL: ProviderId = ProviderId(0);
1065const PROVIDER_ID_INDENT: ProviderId = ProviderId(1);
1066const PROVIDER_ID_MARKDOWN: ProviderId = ProviderId(2);
1067const PROVIDER_ID_SYNTAX: ProviderId = ProviderId(3);
1068const PROVIDER_ID_LSP: ProviderId = ProviderId(4);
1069
1070pub struct ManualPrimary;
1071
1072impl FoldProvider for ManualPrimary {
1073    fn id(&self) -> ProviderId {
1074        PROVIDER_ID_MANUAL
1075    }
1076    fn kind(&self) -> ProviderKind {
1077        ProviderKind::Primary
1078    }
1079    fn compute(&self, _ctx: &FoldContext<'_>) -> Vec<Fold> {
1080        // Manual folds (zf) are carried over by
1081        // `Editor::recompute_folds` from the previous fold
1082        // list; the Primary provider produces nothing.
1083        Vec::new()
1084    }
1085}
1086
1087pub struct IndentPrimary;
1088
1089impl FoldProvider for IndentPrimary {
1090    fn id(&self) -> ProviderId {
1091        PROVIDER_ID_INDENT
1092    }
1093    fn kind(&self) -> ProviderKind {
1094        ProviderKind::Primary
1095    }
1096    fn compute(&self, ctx: &FoldContext<'_>) -> Vec<Fold> {
1097        compute_indent_folds(ctx.buffer, &ctx.indent)
1098    }
1099}
1100
1101pub struct MarkdownPrimary;
1102
1103impl FoldProvider for MarkdownPrimary {
1104    fn id(&self) -> ProviderId {
1105        PROVIDER_ID_MARKDOWN
1106    }
1107    fn kind(&self) -> ProviderKind {
1108        ProviderKind::Primary
1109    }
1110    fn compute(&self, ctx: &FoldContext<'_>) -> Vec<Fold> {
1111        compute_markdown_folds(ctx.buffer)
1112    }
1113}
1114
1115pub struct SyntaxPrimary;
1116
1117impl FoldProvider for SyntaxPrimary {
1118    fn id(&self) -> ProviderId {
1119        PROVIDER_ID_SYNTAX
1120    }
1121    fn kind(&self) -> ProviderKind {
1122        ProviderKind::Primary
1123    }
1124    fn compute(&self, ctx: &FoldContext<'_>) -> Vec<Fold> {
1125        if let Some(syntax) = ctx.syntax
1126            && let Some(folds) = compute_syntax_folds(syntax)
1127        {
1128            return folds;
1129        }
1130        // Cascade: markdown for `.md`, else indent — matches
1131        // pre-refactor `Editor::recompute_syntax_folds`.
1132        let is_md = ctx
1133            .path
1134            .and_then(|p| p.extension())
1135            .and_then(|e| e.to_str())
1136            .map(|ext| ext.eq_ignore_ascii_case("md"))
1137            .unwrap_or(false);
1138        if is_md {
1139            compute_markdown_folds(ctx.buffer)
1140        } else {
1141            compute_indent_folds(ctx.buffer, &ctx.indent)
1142        }
1143    }
1144}
1145
1146pub struct LspPrimary;
1147
1148impl FoldProvider for LspPrimary {
1149    fn id(&self) -> ProviderId {
1150        PROVIDER_ID_LSP
1151    }
1152    fn kind(&self) -> ProviderKind {
1153        ProviderKind::Primary
1154    }
1155    fn compute(&self, ctx: &FoldContext<'_>) -> Vec<Fold> {
1156        if let Some(folds) = ctx.lsp_folds
1157            && !folds.is_empty()
1158        {
1159            return folds.to_vec();
1160        }
1161        // Cascade to syntax provider's behaviour — matches
1162        // pre-refactor `Editor::recompute_lsp_folds`.
1163        SyntaxPrimary.compute(ctx)
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    #![allow(clippy::unwrap_used)]
1170    use super::*;
1171    use lattice_protocol::edit::Edit;
1172    use lattice_protocol::position::Position;
1173
1174    fn buf(text: &str) -> Buffer {
1175        let mut b = Buffer::empty();
1176        if !text.is_empty() {
1177            b.apply_edit(&Edit::insert(Position::ZERO, text.to_string()))
1178                .unwrap();
1179        }
1180        b
1181    }
1182
1183    #[test]
1184    fn empty_buffer_yields_no_folds() {
1185        let b = buf("");
1186        assert!(compute_indent_folds(&b, &IndentUnit::default()).is_empty());
1187    }
1188
1189    #[test]
1190    fn visible_walk_fills_the_viewport_past_a_closed_fold() {
1191        // The reported GPUI bug (2026-08-09): a doc whose headings are
1192        // folded showed content only up to the source line sitting
1193        // `viewport_height` below the scroll, leaving half the screen
1194        // blank — because the visible set was `[scroll, scroll+height)`
1195        // with the folded lines filtered out afterwards, so collapsed
1196        // lines burned viewport slots and everything past the window was
1197        // never looked at.
1198        //
1199        // 100-line buffer, viewport 10, one closed fold swallowing lines
1200        // 1..=79. The pane must still show 10 rows: line 0, the fold
1201        // head at 1, then 80..=87 — NOT "line 0 + head + nothing".
1202        let folds = vec![Fold {
1203            start_line: 1,
1204            end_line: 79,
1205            closed: true,
1206            identity: None,
1207        }];
1208        let idx = FoldIndex::from_folds(&folds, true);
1209        let vis = visible_source_lines(&idx, 0, 10, 100);
1210        assert_eq!(
1211            vis.len(),
1212            10,
1213            "the viewport must be filled with visible rows, not spent on \
1214             collapsed ones: {vis:?}"
1215        );
1216        assert_eq!(vis, vec![0, 1, 80, 81, 82, 83, 84, 85, 86, 87]);
1217    }
1218
1219    /// `fold_aware_visible_end` must agree with the walk that decides
1220    /// which lines are painted: the bound is the line the LAST visible
1221    /// row sits on, not `scroll + height`. Anything sizing a range off
1222    /// the row count — the syntax parse window, the cell matrix's chunk
1223    /// window — stops short of the screen the moment a fold closes.
1224    #[test]
1225    fn fold_aware_end_reaches_the_last_visible_row() {
1226        let folds = vec![Fold {
1227            start_line: 1,
1228            end_line: 79,
1229            closed: true,
1230            identity: None,
1231        }];
1232        let idx = FoldIndex::from_folds(&folds, true);
1233        // Same fixture as the visible walk above: rows end on line 87,
1234        // so the exclusive bound is 88 — not the naive 0 + 10.
1235        assert_eq!(visible_source_lines(&idx, 0, 10, 100).last(), Some(&87));
1236        assert_eq!(fold_aware_visible_end(&idx, 0, 10, 100), 88);
1237
1238        // A viewport ENDING on a closed fold head reaches past the whole
1239        // body: 3 rows are line 0, the head at 1, and 80.
1240        assert_eq!(fold_aware_visible_end(&idx, 0, 2, 100), 80);
1241
1242        // `foldenable` off, and no closed fold at all, are both exactly
1243        // `scroll + height` — the fold-free path callers had before.
1244        let off = FoldIndex::from_folds(&folds, false);
1245        assert_eq!(fold_aware_visible_end(&off, 0, 10, 100), 10);
1246        let none = FoldIndex::from_folds(&[], true);
1247        assert_eq!(fold_aware_visible_end(&none, 5, 10, 100), 15);
1248        // Never past EOF, never below the fold-free answer.
1249        assert_eq!(fold_aware_visible_end(&none, 95, 10, 100), 100);
1250        assert_eq!(fold_aware_visible_end(&idx, 0, 0, 100), 0);
1251
1252        // The slice front-end agrees with the indexed one on every case
1253        // above, including its no-closed-fold short circuit.
1254        assert_eq!(fold_aware_visible_end_of(&folds, true, 0, 10, 100), 88);
1255        assert_eq!(fold_aware_visible_end_of(&folds, false, 0, 10, 100), 10);
1256        assert_eq!(fold_aware_visible_end_of(&[], true, 5, 10, 100), 15);
1257    }
1258
1259    #[test]
1260    fn visible_walk_matches_the_unfolded_case_and_respects_eof() {
1261        let idx = FoldIndex::from_folds(&[], true);
1262        // No folds ⇒ plain contiguous window (the pre-fix behaviour,
1263        // which is why this bug hid until something was folded).
1264        assert_eq!(visible_source_lines(&idx, 5, 4, 100), vec![5, 6, 7, 8]);
1265        // Never walks past EOF, and a scroll at/past EOF yields nothing.
1266        assert_eq!(visible_source_lines(&idx, 98, 10, 100), vec![98, 99]);
1267        assert!(visible_source_lines(&idx, 100, 10, 100).is_empty());
1268        // `foldenable` off ⇒ folds are inert, window stays contiguous.
1269        let folds = vec![Fold {
1270            start_line: 1,
1271            end_line: 50,
1272            closed: true,
1273            identity: None,
1274        }];
1275        let off = FoldIndex::from_folds(&folds, false);
1276        assert_eq!(visible_source_lines(&off, 0, 4, 100), vec![0, 1, 2, 3]);
1277    }
1278
1279    #[test]
1280    fn visible_walk_chains_folds_that_hide_the_next_head() {
1281        // Two closed folds where the second's head is itself hidden by
1282        // the first. The walk must land on the first visible line after
1283        // BOTH, never on a head the user cannot see.
1284        let folds = vec![
1285            Fold {
1286                start_line: 2,
1287                end_line: 6,
1288                closed: true,
1289                identity: None,
1290            },
1291            Fold {
1292                start_line: 4,
1293                end_line: 9,
1294                closed: true,
1295                identity: None,
1296            },
1297        ];
1298        let idx = FoldIndex::from_folds(&folds, true);
1299        let vis = visible_source_lines(&idx, 0, 5, 20);
1300        assert_eq!(vis[0], 0);
1301        assert_eq!(vis[1], 1);
1302        assert_eq!(vis[2], 2, "the outer fold's head stays visible");
1303        assert!(
1304            vis[3] > 6,
1305            "no row may land inside a collapsed region: {vis:?}"
1306        );
1307    }
1308
1309    #[test]
1310    fn fold_summary_text_is_the_one_shared_trailer() {
1311        // Both renderers call this, so pinning the exact string is what
1312        // stops the TUI and GPUI trailers from drifting on spacing or
1313        // wording. The leading space separates it from the heading; the
1314        // `⋯` is U+22EF (one column, plain BMP — renders in every
1315        // terminal font, no nerd-font fallback needed).
1316        assert_eq!(fold_summary_text(4), " ⋯ 4 lines");
1317        assert_eq!(fold_summary_text(1), " ⋯ 1 lines");
1318        assert_eq!(fold_summary_text(120), " ⋯ 120 lines");
1319        // 10 columns for the common 1-digit case — the width the TUI's
1320        // `wrap_cols` seam must keep OUT of the wrap computation.
1321        assert_eq!(fold_summary_text(4).chars().count(), 10);
1322    }
1323
1324    #[test]
1325    fn single_line_yields_no_folds() {
1326        let b = buf("hello");
1327        assert!(compute_indent_folds(&b, &IndentUnit::default()).is_empty());
1328    }
1329
1330    #[test]
1331    fn flat_lines_yield_no_folds() {
1332        let b = buf("a\nb\nc\nd\n");
1333        assert!(compute_indent_folds(&b, &IndentUnit::default()).is_empty());
1334    }
1335
1336    #[test]
1337    fn one_block_produces_a_fold() {
1338        let b = buf("def f():\n    pass\n");
1339        let folds = compute_indent_folds(&b, &IndentUnit::default());
1340        assert_eq!(folds.len(), 1);
1341        let f = &folds[0];
1342        assert_eq!(f.start_line, 0);
1343        assert_eq!(f.end_line, 1);
1344        assert!(!f.closed);
1345    }
1346
1347    /// IG.5: the bug the shared walk fixed. `leading_indent` counted a tab
1348    /// as ONE column, so a tab-indented file's nesting looked shallower than
1349    /// its space-indented twin's and the two folded at different boundaries.
1350    /// Measuring in display columns makes them identical, which is also what
1351    /// keeps `zc` and the indentation guides agreeing about a block.
1352    /// IG.5 equivalence check against REAL content, which I did not do
1353    /// when I replaced the walk.
1354    ///
1355    /// IG.5 swapped `compute_indent_folds` from a scan-forward walk to a
1356    /// stack walk AND changed depth from whitespace characters to display
1357    /// columns. I tested hand-written fixtures and the tab case; I never
1358    /// checked that a real space-indented file folds identically, which
1359    /// is the one property a rewrite of a landed feature owes.
1360    ///
1361    /// `todo.org` is the file from the 2026-08-16 report — 219 lines,
1362    /// no tabs, indents of 0/2/4 — so the column and character measures
1363    /// agree and ONLY the algorithm change is under test.
1364    #[test]
1365    fn indent_folds_match_the_pre_ig5_walk_on_real_content() {
1366        mod pre_ig5 {
1367            fn leading_indent(line: &str) -> usize {
1368                line.chars().take_while(|c| c.is_whitespace()).count()
1369            }
1370            fn is_closer_line(line: &str) -> bool {
1371                let t = line.trim();
1372                !t.is_empty()
1373                    && t.chars()
1374                        .all(|c| matches!(c, ')' | ']' | '}' | ',' | ';' | '?'))
1375            }
1376            fn next_non_blank(indents: &[Option<usize>], from: usize) -> Option<usize> {
1377                indents
1378                    .iter()
1379                    .enumerate()
1380                    .skip(from)
1381                    .find_map(|(i, x)| x.is_some().then_some(i))
1382            }
1383            pub fn old_folds(text: &str) -> Vec<(u32, u32)> {
1384                let lines: Vec<&str> = text.split('\n').collect();
1385                let n = lines.len();
1386                if n <= 1 {
1387                    return vec![];
1388                }
1389                let indents: Vec<Option<usize>> = lines
1390                    .iter()
1391                    .map(|l| {
1392                        if l.trim().is_empty() {
1393                            None
1394                        } else {
1395                            Some(leading_indent(l))
1396                        }
1397                    })
1398                    .collect();
1399                let mut next_nb = vec![n; n];
1400                {
1401                    let mut nx = n;
1402                    for i in (0..n).rev() {
1403                        next_nb[i] = nx;
1404                        if indents[i].is_some() {
1405                            nx = i;
1406                        }
1407                    }
1408                }
1409                let mut out = vec![];
1410                for i in 0..n {
1411                    if out.len() >= 5000 {
1412                        break;
1413                    }
1414                    let Some(start_indent) = indents[i] else {
1415                        continue;
1416                    };
1417                    let j = next_nb[i];
1418                    if j >= n {
1419                        continue;
1420                    }
1421                    let Some(next_indent) = indents[j] else {
1422                        continue;
1423                    };
1424                    if next_indent <= start_indent {
1425                        continue;
1426                    }
1427                    let mut end = j;
1428                    for (k, ind) in indents.iter().enumerate().skip(j + 1) {
1429                        match ind {
1430                            Some(x) if *x > start_indent => end = k,
1431                            Some(_) => break,
1432                            None => continue,
1433                        }
1434                    }
1435                    if let Some(c) = next_non_blank(&indents, end + 1)
1436                        && let Some(ind) = indents[c]
1437                        && ind == start_indent
1438                        && is_closer_line(lines[c])
1439                    {
1440                        end = c;
1441                    }
1442                    out.push((i as u32, end as u32));
1443                }
1444                out
1445            }
1446        }
1447
1448        let text = include_str!("../../../docs/dev/notes/todo.org");
1449        let b = buf(text);
1450        let new: Vec<(u32, u32)> = compute_indent_folds(&b, &IndentUnit::new(4, true, 4))
1451            .iter()
1452            .map(|f| (f.start_line, f.end_line))
1453            .collect();
1454        let old = pre_ig5::old_folds(text);
1455        assert_eq!(
1456            new, old,
1457            "IG.5 must not have changed the fold set on a space-indented file"
1458        );
1459    }
1460
1461    #[test]
1462    fn tab_indented_file_folds_like_its_space_indented_twin() {
1463        let unit = IndentUnit::new(4, false, 4);
1464        let tabbed = buf("fn f() {\n\tif c {\n\t\twork();\n\t}\n}\n");
1465        let spaced = buf("fn f() {\n    if c {\n        work();\n    }\n}\n");
1466        let t: Vec<(u32, u32)> = compute_indent_folds(&tabbed, &unit)
1467            .iter()
1468            .map(|f| (f.start_line, f.end_line))
1469            .collect();
1470        let sp: Vec<(u32, u32)> = compute_indent_folds(&spaced, &unit)
1471            .iter()
1472            .map(|f| (f.start_line, f.end_line))
1473            .collect();
1474        assert_eq!(t, sp);
1475        assert_eq!(
1476            t,
1477            vec![(0, 4), (1, 3)],
1478            "each block swallows its closing brace"
1479        );
1480    }
1481
1482    /// A `tabstop` change moves where a tab lands, and therefore what counts
1483    /// as deeper. The fold walk has to see that, which it could not when it
1484    /// measured characters.
1485    #[test]
1486    fn tabstop_is_honoured_by_the_fold_walk() {
1487        // Line 1 is one tab, line 2 is six spaces. At tabstop=4 the tab is
1488        // four columns, so line 2 is DEEPER and opens a nested block; at
1489        // tabstop=8 it is eight columns, so line 2 is shallower and nests
1490        // nothing. Same bytes, different structure — which the walk could
1491        // not see while it counted whitespace characters.
1492        let b = buf("a\n\tb\n      c\n");
1493        let at4 = compute_indent_folds(&b, &IndentUnit::new(4, false, 4));
1494        let at8 = compute_indent_folds(&b, &IndentUnit::new(4, false, 8));
1495        assert_eq!(at4.len(), 2, "tab == 4 cols: line 2 nests under line 1");
1496        assert_eq!(at8.len(), 1, "tab == 8 cols: line 2 is shallower");
1497    }
1498
1499    #[test]
1500    fn nested_blocks_produce_nested_folds() {
1501        let b = buf("outer:\n    inner:\n        deep\n        deeper\n    after-inner\n");
1502        let folds = compute_indent_folds(&b, &IndentUnit::default());
1503        // outer (0..4) and inner (1..3).
1504        assert!(folds.iter().any(|f| f.start_line == 0 && f.end_line == 4));
1505        assert!(folds.iter().any(|f| f.start_line == 1 && f.end_line == 3));
1506    }
1507
1508    #[test]
1509    fn blank_lines_inside_a_block_dont_break_it() {
1510        let b = buf("def f():\n    line1\n\n    line2\n");
1511        let folds = compute_indent_folds(&b, &IndentUnit::default());
1512        assert_eq!(folds.len(), 1);
1513        // Fold extends to line 3 (last indented row); the blank
1514        // line on row 2 is skipped.
1515        assert_eq!(folds[0].end_line, 3);
1516    }
1517
1518    #[test]
1519    fn blank_lines_at_top_dont_start_a_fold() {
1520        let b = buf("\n    indented\nfollowing\n");
1521        // Line 0 is blank; no fold should start there.
1522        let folds = compute_indent_folds(&b, &IndentUnit::default());
1523        assert!(folds.iter().all(|f| f.start_line != 0));
1524    }
1525
1526    #[test]
1527    fn rust_function_produces_an_indent_fold() {
1528        // Smoke test for the common case: `fn foo() { ... }` should
1529        // produce a fold spanning the body. Default `foldmethod = manual`
1530        // produces no folds; users need `:set foldmethod=indent` (or
1531        // `=syntax` cascading to indent) for `zc` to have something
1532        // to operate on. Closer-line inclusion swallows the trailing
1533        // `}` so the fold extends to the closing brace line.
1534        let src =
1535            "fn outer() {\n    let x = 1;\n    if x > 0 {\n        println!(\"yes\");\n    }\n}\n";
1536        let b = buf(src);
1537        let folds = compute_indent_folds(&b, &IndentUnit::default());
1538        let outer = folds
1539            .iter()
1540            .find(|f| f.start_line == 0)
1541            .expect("outer fn fold missing");
1542        assert_eq!(
1543            outer.end_line, 5,
1544            "outer fold should swallow the final `}}` line: {outer:?}"
1545        );
1546        let inner = folds
1547            .iter()
1548            .find(|f| f.start_line == 2)
1549            .expect("inner if fold missing");
1550        assert_eq!(
1551            inner.end_line, 4,
1552            "inner fold should swallow its `}}` at indent 4: {inner:?}"
1553        );
1554    }
1555
1556    #[test]
1557    fn computed_folds_are_open_by_default() {
1558        let b = buf("a:\n    b\n");
1559        let folds = compute_indent_folds(&b, &IndentUnit::default());
1560        assert!(!folds[0].closed);
1561    }
1562
1563    // --- Markdown provider --------------------------------------
1564
1565    #[test]
1566    fn markdown_empty_buffer_yields_no_folds() {
1567        let b = buf("");
1568        assert!(compute_markdown_folds(&b).is_empty());
1569    }
1570
1571    #[test]
1572    fn markdown_no_headings_yields_no_folds() {
1573        let b = buf("just\nsome\nbody text\n");
1574        assert!(compute_markdown_folds(&b).is_empty());
1575    }
1576
1577    #[test]
1578    fn markdown_single_heading_with_body_folds_to_eob() {
1579        let b = buf("# Title\nbody line one\nbody line two\n");
1580        let folds = compute_markdown_folds(&b);
1581        assert_eq!(folds.len(), 1);
1582        let f = &folds[0];
1583        assert_eq!(f.start_line, 0);
1584        // Trailing newline produces an empty 4th line (idx 3); the
1585        // fold spans through the last *real* line (idx 2).
1586        assert!(f.end_line >= 2);
1587        assert!(!f.closed);
1588    }
1589
1590    #[test]
1591    fn markdown_single_line_heading_skipped() {
1592        // Two H1s back-to-back: the first has no body, so no fold.
1593        let b = buf("# A\n# B\nbody\n");
1594        let folds = compute_markdown_folds(&b);
1595        assert_eq!(folds.len(), 1);
1596        assert_eq!(folds[0].start_line, 1);
1597    }
1598
1599    #[test]
1600    fn markdown_h1_then_h2_nests() {
1601        let b = buf("# Outer\nlead\n## Inner\nbody\n");
1602        let folds = compute_markdown_folds(&b);
1603        // Outer (line 0) folds to end; Inner (line 2) folds inside it.
1604        assert!(folds.iter().any(|f| f.start_line == 0));
1605        assert!(folds.iter().any(|f| f.start_line == 2));
1606    }
1607
1608    #[test]
1609    fn markdown_h2_ends_at_next_h1() {
1610        let b = buf("# A\n## A.1\na1 body\n# B\nb body\n");
1611        let folds = compute_markdown_folds(&b);
1612        // ## A.1 fold should end at line 2 (line before # B).
1613        let inner = folds
1614            .iter()
1615            .find(|f| f.start_line == 1)
1616            .expect("expected ## fold");
1617        assert_eq!(inner.end_line, 2);
1618    }
1619
1620    #[test]
1621    fn markdown_hash_inside_code_fence_not_a_heading() {
1622        let b = buf("# Real\nbody\n```\n# inside fence\n```\nafter\n");
1623        let folds = compute_markdown_folds(&b);
1624        // Only one heading (line 0) should be detected.
1625        assert_eq!(folds.len(), 1);
1626        assert_eq!(folds[0].start_line, 0);
1627    }
1628
1629    #[test]
1630    fn markdown_tilde_fence_also_protects_hashes() {
1631        let b = buf("# Real\n~~~\n# inside\n~~~\nafter\n");
1632        let folds = compute_markdown_folds(&b);
1633        assert_eq!(folds.len(), 1);
1634        assert_eq!(folds[0].start_line, 0);
1635    }
1636
1637    #[test]
1638    fn markdown_indented_4_spaces_is_not_a_heading() {
1639        // 4+ leading spaces means indented code block, not heading.
1640        let b = buf("body\n    # not heading\nmore body\n");
1641        let folds = compute_markdown_folds(&b);
1642        assert!(folds.is_empty());
1643    }
1644
1645    #[test]
1646    fn markdown_3_leading_spaces_still_a_heading() {
1647        let b = buf("   # Heading\nbody\n");
1648        let folds = compute_markdown_folds(&b);
1649        assert_eq!(folds.len(), 1);
1650    }
1651
1652    #[test]
1653    fn markdown_seven_hashes_not_a_heading() {
1654        // CommonMark caps at 6 hashes.
1655        let b = buf("####### nope\nbody\n");
1656        let folds = compute_markdown_folds(&b);
1657        assert!(folds.is_empty());
1658    }
1659
1660    #[test]
1661    fn markdown_hash_without_space_not_a_heading() {
1662        let b = buf("#nospace\nbody\n");
1663        let folds = compute_markdown_folds(&b);
1664        assert!(folds.is_empty());
1665    }
1666
1667    #[test]
1668    fn markdown_open_by_default() {
1669        let b = buf("# H\nbody\n");
1670        let folds = compute_markdown_folds(&b);
1671        assert!(!folds[0].closed);
1672    }
1673
1674    #[test]
1675    fn atx_depth_recognises_levels_one_through_six() {
1676        for n in 1..=6u32 {
1677            let prefix: String = "#".repeat(n as usize);
1678            let line = format!("{prefix} text");
1679            assert_eq!(atx_heading_depth(&line), Some(n));
1680        }
1681    }
1682
1683    #[test]
1684    fn atx_depth_rejects_zero_or_seven() {
1685        assert_eq!(atx_heading_depth("nothing"), None);
1686        assert_eq!(atx_heading_depth("####### too deep"), None);
1687    }
1688
1689    #[test]
1690    fn atx_depth_accepts_hash_alone() {
1691        // CommonMark: "# " or just "#" both valid.
1692        assert_eq!(atx_heading_depth("#"), Some(1));
1693    }
1694
1695    // --- Syntax (tree-sitter) provider --------------------------
1696
1697    /// Regression guard: `compute_syntax_folds` must be ~O(n), not
1698    /// O(folds × file_size). The old per-fold `line_at` rescanned the source
1699    /// from byte 0 on every call, so a large file (dispatch.rs, 36k lines,
1700    /// thousands of folds) took ~34 s and froze the editor. A file with
1701    /// thousands of foldable items must compute well under a second.
1702    #[test]
1703    fn syntax_folds_are_linear_not_quadratic() {
1704        let mut src = String::with_capacity(200_000);
1705        for i in 0..3000 {
1706            src.push_str(&format!(
1707                "fn f{i}() {{\n    let x = {i};\n    x + 1\n}}\n\n"
1708            ));
1709        }
1710        let syntax = rust_syntax_with(&src);
1711        let t = std::time::Instant::now();
1712        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds.scm");
1713        let ms = t.elapsed().as_secs_f64() * 1000.0;
1714        assert!(
1715            folds.len() >= 3000,
1716            "each fn body should fold (got {})",
1717            folds.len()
1718        );
1719        assert!(
1720            ms < 2000.0,
1721            "fold compute must stay ~linear; took {ms:.0}ms — an O(n²) regression \
1722             (per-fold full-source rescan) would take many seconds here"
1723        );
1724    }
1725
1726    fn rust_syntax_with(text: &str) -> lattice_syntax::Syntax {
1727        let mut s = lattice_syntax::Syntax::for_language(lattice_syntax::Lang::Rust)
1728            .unwrap()
1729            .unwrap();
1730        s.parse(text);
1731        s
1732    }
1733
1734    fn markdown_syntax_with(text: &str) -> lattice_syntax::Syntax {
1735        let mut s = lattice_syntax::Syntax::for_language(lattice_syntax::Lang::Markdown)
1736            .unwrap()
1737            .unwrap();
1738        s.parse(text);
1739        s
1740    }
1741
1742    #[test]
1743    fn rust_syntax_folds_function_struct_and_impl() {
1744        let src = r#"struct Buffer {
1745    rope: Rope,
1746}
1747
1748impl Buffer {
1749    fn new() -> Self {
1750        Self { rope: Rope::new() }
1751    }
1752}
1753"#;
1754        let syntax = rust_syntax_with(src);
1755        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds.scm");
1756        // struct_item: lines 0..=2
1757        assert!(
1758            folds.iter().any(|f| f.start_line == 0 && f.end_line >= 2),
1759            "expected struct fold: {folds:?}"
1760        );
1761        // impl_item: starts at line 4
1762        assert!(
1763            folds.iter().any(|f| f.start_line == 4),
1764            "expected impl fold: {folds:?}"
1765        );
1766        // function_item: starts at line 5 (`fn new`)
1767        assert!(
1768            folds.iter().any(|f| f.start_line == 5),
1769            "expected fn fold: {folds:?}"
1770        );
1771    }
1772
1773    #[test]
1774    fn rust_syntax_folds_skips_single_line_items() {
1775        let src = "use std::sync::Arc;\nfn main() {}\n";
1776        let syntax = rust_syntax_with(src);
1777        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds");
1778        // Both items live on a single line; nothing to fold.
1779        assert!(
1780            folds.iter().all(|f| f.end_line > f.start_line),
1781            "single-line items should not produce folds: {folds:?}"
1782        );
1783    }
1784
1785    #[test]
1786    fn rust_syntax_folds_let_with_if_else_expression() {
1787        // The user's exact scenario: a `let` binding with an
1788        // if-else expression body. Three folds must be available --
1789        // the then-block, the else-block, AND the surrounding
1790        // if_expression -- so progressive `zc`s walk inner → outer.
1791        // Wrap in a fn body so tree-sitter parses it cleanly even
1792        // without a trailing `;` -- the original report omitted it.
1793        let src = "fn outer() {\n    let len = if cond {\n        a\n    } else {\n        b\n    };\n}\n";
1794        let syntax = rust_syntax_with(src);
1795        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds");
1796        // then-block fold: starts on line 1 (the `{` after `if cond`).
1797        assert!(
1798            folds.iter().any(|f| f.start_line == 1 && f.end_line == 3),
1799            "expected then-block fold at 1..=3: {folds:?}"
1800        );
1801        // else-block fold: starts on line 3 (the `} else {`).
1802        assert!(
1803            folds.iter().any(|f| f.start_line == 3 && f.end_line == 5),
1804            "expected else-block fold at 3..=5: {folds:?}"
1805        );
1806        // if_expression covers lines 1..=5 (`if ... else { ... }`).
1807        assert!(
1808            folds.iter().any(|f| f.start_line == 1 && f.end_line == 5),
1809            "expected if_expression fold at 1..=5: {folds:?}"
1810        );
1811    }
1812
1813    #[test]
1814    fn rust_syntax_folds_if_else_without_trailing_semicolon() {
1815        // The literal no-semicolon shape the user shared. A `let`
1816        // without `;` is not a valid Rust statement but tree-sitter
1817        // still recovers and the if_expression node exists. Verify
1818        // it produces a fold so progressive `zc` can reach the
1819        // outer if/else as one unit.
1820        let src = "fn outer() -> u32 {\n    let len = if cond {\n        bytes - 1\n    } else {\n        bytes\n    }\n}\n";
1821        let syntax = rust_syntax_with(src);
1822        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds");
1823        // if_expression fold (the user's "fold the if part" target)
1824        // starts on line 1 and runs through line 5 (the closing `}`
1825        // of the else branch).
1826        assert!(
1827            folds.iter().any(|f| f.start_line == 1 && f.end_line == 5),
1828            "expected if_expression fold at 1..=5: {folds:?}"
1829        );
1830    }
1831
1832    #[test]
1833    fn rust_syntax_top_level_let_with_if_else_emits_five_line_fold() {
1834        // The literal snippet from the user's report -- no
1835        // surrounding fn, no semicolon. tree-sitter recovers and
1836        // emits the if_expression node; folds.scm captures it. The
1837        // outermost fold starting at line 0 must span the full 5
1838        // lines so `zc` on the `let` line collapses the entire
1839        // form in one step and the renderer reports "5 lines
1840        // folded".
1841        let src = "let len = if has_trailing_newline {\n    bytes - 1\n} else {\n    bytes\n}\n";
1842        let syntax = rust_syntax_with(src);
1843        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds");
1844        let widest_at_zero = folds
1845            .iter()
1846            .filter(|f| f.start_line == 0)
1847            .max_by_key(|f| f.end_line)
1848            .expect("a fold must start at line 0");
1849        assert_eq!(
1850            widest_at_zero.end_line, 4,
1851            "outermost fold at line 0 must end at line 4 (5 lines): {folds:?}"
1852        );
1853    }
1854
1855    #[test]
1856    fn rust_syntax_folds_block_comments() {
1857        // Multi-line `/* ... */` comments fall under the
1858        // `block_comment` capture in folds.scm.
1859        let src = "/*\n * doc\n */\nfn main() {}\n";
1860        let syntax = rust_syntax_with(src);
1861        let folds = compute_syntax_folds(syntax.snapshot()).expect("rust folds");
1862        assert!(
1863            folds.iter().any(|f| f.start_line == 0 && f.end_line == 2),
1864            "expected block_comment fold: {folds:?}"
1865        );
1866    }
1867
1868    #[test]
1869    fn rust_syntax_folds_identities_separate_struct_from_fn() {
1870        // Two items with the same start-line text but different
1871        // node kinds must produce distinct identities so closed-
1872        // state can't mistakenly transfer between them.
1873        let a = rust_syntax_with("struct X {\n    f: u8,\n}\n");
1874        let b = rust_syntax_with("fn x() {\n    return;\n}\n");
1875        let fa = compute_syntax_folds(a.snapshot()).unwrap();
1876        let fb = compute_syntax_folds(b.snapshot()).unwrap();
1877        let id_a = fa.iter().find_map(|f| f.identity).expect("a id");
1878        let id_b = fb.iter().find_map(|f| f.identity).expect("b id");
1879        assert_ne!(
1880            id_a, id_b,
1881            "node kind must contribute to identity (struct vs fn)"
1882        );
1883    }
1884
1885    #[test]
1886    fn markdown_syntax_folds_section() {
1887        let src = "# H1\nbody one\nbody two\n# H2\nafter\n";
1888        let syntax = markdown_syntax_with(src);
1889        let folds = compute_syntax_folds(syntax.snapshot()).expect("markdown folds");
1890        // Each section becomes a fold; the H1 section spans lines
1891        // 0..=2 (heading + 2 body lines, before the H2 sibling).
1892        assert!(
1893            folds.iter().any(|f| f.start_line == 0),
1894            "expected H1 section fold: {folds:?}"
1895        );
1896    }
1897
1898    #[test]
1899    fn markdown_code_block_lines_cover_the_fence_only() {
1900        // 0: prose  1: ```rust  2: content  3: ```  4: prose
1901        let src = "text before\n```rust\nfn main() {}\n```\ntext after\n";
1902        let syntax = markdown_syntax_with(src);
1903        let lines = compute_code_block_lines(syntax.snapshot()).expect("markdown code-block lines");
1904        for tinted in [1u32, 2, 3] {
1905            assert!(
1906                lines.contains(&tinted),
1907                "line {tinted} is inside the fence and must be tinted: {lines:?}"
1908            );
1909        }
1910        for prose in [0u32, 4] {
1911            assert!(
1912                !lines.contains(&prose),
1913                "line {prose} is prose and must NOT be tinted: {lines:?}"
1914            );
1915        }
1916    }
1917
1918    #[test]
1919    fn code_block_lines_none_without_a_codeblock_capture() {
1920        // Rust's folds query has `@fold` but no `@codeblock` capture, so the
1921        // capture-driven detection reports no code-block tint — the contract
1922        // that replaced the hardcoded markdown node-kind list. A grammar earns
1923        // the tint only by declaring `@codeblock` (markdown does; a plugin
1924        // grammar such as org declares it the same way).
1925        let syntax = rust_syntax_with("fn main() {\n    let x = 1;\n}\n");
1926        assert!(
1927            compute_code_block_lines(syntax.snapshot()).is_none(),
1928            "a language without a @codeblock capture must have no code-block tint"
1929        );
1930    }
1931
1932    #[test]
1933    fn syntax_folds_returns_none_for_plain_buffer() {
1934        // Plain language: there's no Syntax instance to begin with;
1935        // the App-level cascade is what handles plain. The provider
1936        // function itself only runs when called with a Syntax for a
1937        // recognised language. We assert the registry honestly
1938        // reports no folds.scm for the inline grammar here as a
1939        // proxy: it has a parser+tree but no folds query.
1940        let src = "*emphasis* and `code`";
1941        let mut s = lattice_syntax::Syntax::for_language(lattice_syntax::Lang::Markdown)
1942            .unwrap()
1943            .unwrap();
1944        s.parse(src);
1945        // The registry-level guard: if we ask via folds_query for
1946        // a language that doesn't ship one, we get None. (Markdown
1947        // does ship one; the inline grammar doesn't, and we don't
1948        // expose Lang::MarkdownInline as a top-level language.)
1949        let _ = s.tree();
1950    }
1951
1952    // ---- FoldIndex (perf plan C) ---------------------------------
1953
1954    fn closed(start: u32, end: u32) -> Fold {
1955        Fold {
1956            start_line: start,
1957            end_line: end,
1958            closed: true,
1959            identity: None,
1960        }
1961    }
1962    fn open(start: u32, end: u32) -> Fold {
1963        Fold {
1964            start_line: start,
1965            end_line: end,
1966            closed: false,
1967            identity: None,
1968        }
1969    }
1970
1971    #[test]
1972    fn fold_index_empty_returns_false_everywhere() {
1973        let idx = FoldIndex::from_folds(&[], true);
1974        assert!(!idx.line_inside_closed_fold(0));
1975        assert!(!idx.line_inside_closed_fold(10));
1976        assert!(!idx.closed_fold_start_at(0));
1977        assert!(!idx.fold_start_at_any(0));
1978    }
1979
1980    #[test]
1981    fn fold_index_respects_foldenable_off() {
1982        // Even with closed folds present, foldenable=false makes
1983        // every predicate return false — matches the existing
1984        // Editor::* helpers.
1985        let folds = vec![closed(5, 10)];
1986        let idx = FoldIndex::from_folds(&folds, false);
1987        assert!(!idx.line_inside_closed_fold(7));
1988        assert!(!idx.closed_fold_start_at(5));
1989        assert!(!idx.fold_start_at_any(5));
1990        assert_eq!(idx.fold_start_kind_at(5), None);
1991    }
1992
1993    #[test]
1994    fn fold_start_kind_distinguishes_open_closed_and_none() {
1995        // A gutter renderer shows `▾` on open heads and `▸` on closed
1996        // ones; every non-head line reports `None`. foldenable gates it.
1997        let folds = vec![closed(2, 5), open(8, 12)];
1998        let idx = FoldIndex::from_folds(&folds, true);
1999        assert_eq!(idx.fold_start_kind_at(2), Some(FoldMarker::Closed));
2000        assert_eq!(idx.fold_start_kind_at(8), Some(FoldMarker::Open));
2001        // Interior + unrelated lines: no marker.
2002        assert_eq!(idx.fold_start_kind_at(3), None);
2003        assert_eq!(idx.fold_start_kind_at(0), None);
2004        assert_eq!(idx.fold_start_kind_at(12), None);
2005        // foldenable off suppresses the marker on the same heads.
2006        let off = FoldIndex::from_folds(&folds, false);
2007        assert_eq!(off.fold_start_kind_at(2), None);
2008        assert_eq!(off.fold_start_kind_at(8), None);
2009    }
2010
2011    #[test]
2012    fn fold_index_matches_naive_lookup_on_non_overlapping() {
2013        let folds = vec![closed(2, 5), open(8, 12), closed(15, 20)];
2014        let idx = FoldIndex::from_folds(&folds, true);
2015        // Naive walk for the same predicates, asserting parity at
2016        // every line in the relevant range.
2017        let naive_inside = |line: u32| -> bool {
2018            folds
2019                .iter()
2020                .any(|f| f.closed && line > f.start_line && line <= f.end_line)
2021        };
2022        let naive_closed_start =
2023            |line: u32| -> bool { folds.iter().any(|f| f.closed && f.start_line == line) };
2024        let naive_any_start = |line: u32| -> bool { folds.iter().any(|f| f.start_line == line) };
2025        for line in 0..25 {
2026            assert_eq!(
2027                idx.line_inside_closed_fold(line),
2028                naive_inside(line),
2029                "line_inside parity at line {line}"
2030            );
2031            assert_eq!(
2032                idx.closed_fold_start_at(line),
2033                naive_closed_start(line),
2034                "closed_start parity at line {line}"
2035            );
2036            assert_eq!(
2037                idx.fold_start_at_any(line),
2038                naive_any_start(line),
2039                "any_start parity at line {line}"
2040            );
2041        }
2042    }
2043
2044    #[test]
2045    fn fold_index_handles_nested_closed_folds() {
2046        // Outer + inner both closed. The semantics — matching the
2047        // existing `Editor::line_inside_closed_fold` — are: `inside`
2048        // is true iff ANY closed fold has `start < line && line <= end`.
2049        // For a `(0..=10, 3..=7)` nested layout, the inner's start
2050        // line (3) IS inside the outer (because `0 < 3 && 3 <= 10`)
2051        // — the renderer hides it as part of the outer fold's body.
2052        // Only the outer's heading (line 0) and lines past the outer's
2053        // end escape.
2054        let folds = vec![closed(0, 10), closed(3, 7)];
2055        let idx = FoldIndex::from_folds(&folds, true);
2056        assert!(
2057            !idx.line_inside_closed_fold(0),
2058            "outer start row stays visible"
2059        );
2060        for line in 1..=10 {
2061            assert!(
2062                idx.line_inside_closed_fold(line),
2063                "line {line} should be inside the outer fold (0..=10)"
2064            );
2065        }
2066        assert!(!idx.line_inside_closed_fold(11), "past outer end");
2067    }
2068
2069    #[test]
2070    fn fold_index_handles_sibling_nested_only_inner_closed() {
2071        // Outer open + inner closed. Inside the inner range: hidden.
2072        // Outside the inner range: outer is open, so visible.
2073        let folds = vec![open(0, 10), closed(3, 7)];
2074        let idx = FoldIndex::from_folds(&folds, true);
2075        for line in 0..=3 {
2076            assert!(
2077                !idx.line_inside_closed_fold(line),
2078                "line {line} should be visible (outer open, before inner start)"
2079            );
2080        }
2081        for line in 4..=7 {
2082            assert!(
2083                idx.line_inside_closed_fold(line),
2084                "line {line} should be inside the closed inner (3..=7)"
2085            );
2086        }
2087        for line in 8..=11 {
2088            assert!(
2089                !idx.line_inside_closed_fold(line),
2090                "line {line} should be visible (outer open, past inner end)"
2091            );
2092        }
2093    }
2094
2095    #[test]
2096    fn fold_index_handles_overlapping_closed_folds_via_slow_path() {
2097        // Overlapping (rare — manually created) folds should still
2098        // report `inside` for any line covered by either. The
2099        // partition-point fast path exits early on the rightmost
2100        // candidate; this test exercises the slow-path fallback.
2101        let folds = vec![closed(0, 8), closed(2, 4)];
2102        let idx = FoldIndex::from_folds(&folds, true);
2103        // Line 6: outside the inner (2..=4) but inside the outer
2104        // (0..=8). The rightmost candidate by start (inner, idx-1)
2105        // has end=4, so the fast path returns false; the slow path
2106        // walks left and finds the outer.
2107        assert!(idx.line_inside_closed_fold(6));
2108        assert!(idx.line_inside_closed_fold(8));
2109        assert!(!idx.line_inside_closed_fold(9));
2110    }
2111
2112    #[test]
2113    fn fold_index_closed_fold_at_returns_range_only_for_closed_starts() {
2114        // Closed folds get their (start, end) back; open folds and
2115        // non-start lines return None even if their start coincides
2116        // with another fold's interior.
2117        let folds = vec![closed(2, 5), open(8, 12), closed(15, 20)];
2118        let idx = FoldIndex::from_folds(&folds, true);
2119        assert_eq!(idx.closed_fold_at(2), Some((2, 5)));
2120        assert_eq!(idx.closed_fold_at(15), Some((15, 20)));
2121        // Open fold's start: None (closed-only accessor).
2122        assert_eq!(idx.closed_fold_at(8), None);
2123        // Non-start lines (interior or unrelated): None.
2124        assert_eq!(idx.closed_fold_at(3), None);
2125        assert_eq!(idx.closed_fold_at(0), None);
2126        assert_eq!(idx.closed_fold_at(100), None);
2127    }
2128
2129    #[test]
2130    fn fold_index_closed_fold_at_respects_foldenable_off() {
2131        let folds = vec![closed(2, 5)];
2132        let idx = FoldIndex::from_folds(&folds, false);
2133        assert_eq!(idx.closed_fold_at(2), None);
2134    }
2135
2136    #[test]
2137    fn fold_index_enclosing_closed_fold_returns_innermost_range() {
2138        // Non-overlapping: the body of each closed fold reports its
2139        // own range; heads and gaps report None.
2140        let folds = vec![closed(2, 5), open(8, 12), closed(15, 20)];
2141        let idx = FoldIndex::from_folds(&folds, true);
2142        assert_eq!(idx.enclosing_closed_fold(3), Some((2, 5)));
2143        assert_eq!(idx.enclosing_closed_fold(5), Some((2, 5)));
2144        assert_eq!(
2145            idx.enclosing_closed_fold(2),
2146            None,
2147            "head row is not interior"
2148        );
2149        assert_eq!(idx.enclosing_closed_fold(6), None, "gap after fold");
2150        assert_eq!(
2151            idx.enclosing_closed_fold(10),
2152            None,
2153            "open fold body excluded"
2154        );
2155        assert_eq!(idx.enclosing_closed_fold(18), Some((15, 20)));
2156    }
2157
2158    #[test]
2159    fn fold_index_enclosing_closed_fold_prefers_inner_then_climbs_to_outer() {
2160        // Nested closed folds: a body line resolves to the innermost
2161        // fold; jumping to that fold's start then resolves to the
2162        // outer (the scroll walk climbs head-to-head this way).
2163        let folds = vec![closed(0, 10), closed(3, 7)];
2164        let idx = FoldIndex::from_folds(&folds, true);
2165        assert_eq!(
2166            idx.enclosing_closed_fold(5),
2167            Some((3, 7)),
2168            "innermost first"
2169        );
2170        assert_eq!(
2171            idx.enclosing_closed_fold(3),
2172            Some((0, 10)),
2173            "inner head is itself inside the outer ⇒ climb out"
2174        );
2175        assert_eq!(idx.enclosing_closed_fold(0), None, "outer head is visible");
2176        // A line inside only the outer (past the inner's end).
2177        assert_eq!(idx.enclosing_closed_fold(9), Some((0, 10)));
2178    }
2179
2180    #[test]
2181    fn fold_index_enclosing_closed_fold_overlap_slow_path() {
2182        // Overlapping (manually created) folds: line 6 is outside the
2183        // inner (2..=4) but inside the outer (0..=8); the fast path's
2184        // rightmost candidate misses, the slow path finds the outer.
2185        let folds = vec![closed(0, 8), closed(2, 4)];
2186        let idx = FoldIndex::from_folds(&folds, true);
2187        assert_eq!(idx.enclosing_closed_fold(6), Some((0, 8)));
2188        assert_eq!(
2189            idx.enclosing_closed_fold(3),
2190            Some((2, 4)),
2191            "innermost on overlap"
2192        );
2193    }
2194
2195    /// A visible line is its own anchor; a swallowed one reports the head it
2196    /// is drawn on.
2197    #[test]
2198    fn fold_index_visible_anchor_maps_a_hidden_line_to_its_head() {
2199        let folds = vec![closed(10, 30)];
2200        let idx = FoldIndex::from_folds(&folds, true);
2201        assert_eq!(idx.visible_anchor(5), 5, "outside the fold");
2202        assert_eq!(idx.visible_anchor(10), 10, "the head is visible");
2203        assert_eq!(
2204            idx.visible_anchor(20),
2205            10,
2206            "an interior line draws on the head"
2207        );
2208        assert_eq!(idx.visible_anchor(30), 10, "…including the last one");
2209        assert_eq!(idx.visible_anchor(31), 31, "past the end");
2210    }
2211
2212    /// **The reason this climbs instead of asking once.** A sub-fold's head is
2213    /// itself hidden when its parent is also closed, so the innermost
2214    /// encloser is not necessarily on screen — only the outermost is.
2215    #[test]
2216    fn fold_index_visible_anchor_climbs_to_the_outermost_closed_fold() {
2217        let folds = vec![closed(0, 40), closed(10, 30), closed(12, 20)];
2218        let idx = FoldIndex::from_folds(&folds, true);
2219        assert_eq!(
2220            idx.visible_anchor(15),
2221            0,
2222            "three deep, and only the outermost head is actually drawn"
2223        );
2224        assert_eq!(idx.visible_anchor(10), 0, "a nested head is hidden too");
2225    }
2226
2227    /// An open parent does not hide its closed child's head.
2228    #[test]
2229    fn fold_index_visible_anchor_stops_at_an_open_parent() {
2230        let folds = vec![open(0, 40), closed(12, 20)];
2231        let idx = FoldIndex::from_folds(&folds, true);
2232        assert_eq!(
2233            idx.visible_anchor(15),
2234            12,
2235            "the closed child's head is on screen"
2236        );
2237        assert_eq!(idx.visible_anchor(35), 35, "open folds hide nothing");
2238    }
2239
2240    #[test]
2241    fn fold_index_visible_anchor_is_identity_with_foldenable_off() {
2242        let folds = vec![closed(0, 40), closed(10, 30)];
2243        let idx = FoldIndex::from_folds(&folds, false);
2244        for line in [0, 10, 15, 30, 41] {
2245            assert_eq!(idx.visible_anchor(line), line);
2246        }
2247    }
2248
2249    #[test]
2250    fn fold_index_enclosing_closed_fold_respects_foldenable_off() {
2251        let folds = vec![closed(2, 5)];
2252        let idx = FoldIndex::from_folds(&folds, false);
2253        assert_eq!(idx.enclosing_closed_fold(3), None);
2254    }
2255
2256    #[test]
2257    fn fold_index_open_folds_excluded_from_inside_check() {
2258        let folds = vec![open(0, 10)];
2259        let idx = FoldIndex::from_folds(&folds, true);
2260        // Open folds don't hide content — `inside` only applies to
2261        // CLOSED folds.
2262        for line in 0..=10 {
2263            assert!(!idx.line_inside_closed_fold(line));
2264        }
2265        assert!(!idx.closed_fold_start_at(0));
2266        // ...but the start row is still a fold start (for the gutter
2267        // glyph that distinguishes open vs closed).
2268        assert!(idx.fold_start_at_any(0));
2269    }
2270
2271    // ── FL.1: fold levels ─────────────────────────────────
2272
2273    fn f(start: u32, end: u32) -> Fold {
2274        Fold {
2275            start_line: start,
2276            end_line: end,
2277            closed: false,
2278            identity: None,
2279        }
2280    }
2281
2282    #[test]
2283    fn a_flat_set_of_folds_is_all_level_one() {
2284        let folds = vec![f(0, 3), f(5, 8), f(10, 12)];
2285        assert_eq!(fold_levels(&folds), vec![1, 1, 1]);
2286    }
2287
2288    #[test]
2289    fn a_nested_fold_is_one_level_deeper_than_its_parent() {
2290        // outer 0..10, inner 2..5, innermost 3..4
2291        let folds = vec![f(0, 10), f(2, 5), f(3, 4)];
2292        assert_eq!(fold_levels(&folds), vec![1, 2, 3]);
2293    }
2294
2295    /// The multibuffer case that makes proper containment necessary: a
2296    /// file with exactly one excerpt yields a file fold and an excerpt
2297    /// fold over the identical range. If equal ranges counted as
2298    /// nesting, both would be level 2 and `foldlevel=1` would collapse
2299    /// a view with only one level of structure.
2300    #[test]
2301    fn folds_over_the_identical_range_are_siblings_not_parent_and_child() {
2302        let folds = vec![f(0, 4), f(0, 4)];
2303        assert_eq!(fold_levels(&folds), vec![1, 1]);
2304    }
2305
2306    #[test]
2307    fn level_is_independent_of_the_order_folds_arrive_in() {
2308        let ordered = vec![f(0, 10), f(2, 5)];
2309        let reversed = vec![f(2, 5), f(0, 10)];
2310        assert_eq!(fold_levels(&ordered), vec![1, 2]);
2311        assert_eq!(fold_levels(&reversed), vec![2, 1]);
2312    }
2313
2314    #[test]
2315    fn foldlevel_zero_closes_everything() {
2316        let mut folds = vec![f(0, 10), f(2, 5)];
2317        apply_fold_level(&mut folds, 0);
2318        assert!(folds.iter().all(|x| x.closed));
2319    }
2320
2321    #[test]
2322    fn foldlevel_one_keeps_the_outermost_open() {
2323        let mut folds = vec![f(0, 10), f(2, 5)];
2324        apply_fold_level(&mut folds, 1);
2325        assert!(!folds[0].closed, "level 1 fold stays open at foldlevel=1");
2326        assert!(folds[1].closed, "level 2 fold closes at foldlevel=1");
2327    }
2328
2329    #[test]
2330    fn a_high_foldlevel_opens_everything() {
2331        let mut folds = vec![f(0, 10), f(2, 5), f(3, 4)];
2332        apply_fold_level(&mut folds, 99);
2333        assert!(folds.iter().all(|x| !x.closed));
2334    }
2335
2336    /// `apply_fold_level` is a bulk action, so it reopens folds the user
2337    /// had closed by hand — that is what `:set foldlevel=N` means. The
2338    /// rebuild path must NOT do that, which is why it has its own entry
2339    /// point.
2340    #[test]
2341    fn the_rebuild_path_leaves_carried_over_state_alone() {
2342        let mut folds = vec![f(0, 10), f(2, 5)];
2343        folds[0].closed = true; // user pressed `za` on the outer fold
2344        apply_fold_level_to_new(&mut folds, &[true, false], 99);
2345        assert!(
2346            folds[0].closed,
2347            "a fold whose state was carried over keeps it"
2348        );
2349        assert!(!folds[1].closed, "a new fold obeys foldlevel");
2350    }
2351
2352    /// diff-mode emits its unchanged-region folds already closed — that
2353    /// is the feature. `foldlevel` must not reopen them: at the default
2354    /// level a level-1 fold is not deeper than 99, so an assignment
2355    /// (rather than an OR) silently unfolded every collapsed stretch in
2356    /// every diff. Caught by `both_diff_panes_fold_unchanged_after_activation`.
2357    #[test]
2358    fn a_provider_fold_that_arrives_closed_stays_closed() {
2359        let mut folds = vec![f(0, 8), f(22, 30)];
2360        folds[0].closed = true;
2361        folds[1].closed = true;
2362        apply_fold_level_to_new(&mut folds, &[false, false], 99);
2363        assert!(
2364            folds.iter().all(|x| x.closed),
2365            "foldlevel may add a close, never remove one a provider asked for"
2366        );
2367    }
2368
2369    /// ...and the level still closes a new fold that arrived open.
2370    #[test]
2371    fn the_rebuild_path_still_closes_what_is_too_deep() {
2372        let mut folds = vec![f(0, 10), f(2, 5)];
2373        apply_fold_level_to_new(&mut folds, &[false, false], 1);
2374        assert!(!folds[0].closed);
2375        assert!(folds[1].closed);
2376    }
2377
2378    #[test]
2379    fn max_level_reports_the_deepest_nesting() {
2380        assert_eq!(max_fold_level(&[]), 0);
2381        assert_eq!(max_fold_level(&[f(0, 10)]), 1);
2382        assert_eq!(max_fold_level(&[f(0, 10), f(2, 5), f(3, 4)]), 3);
2383    }
2384
2385    /// The early-out must agree with the full computation, or the
2386    /// default path would quietly diverge from the explicit one.
2387    #[test]
2388    fn the_early_out_agrees_with_the_full_pass() {
2389        let cases: Vec<Vec<Fold>> = vec![
2390            vec![],
2391            vec![f(0, 10)],
2392            vec![f(0, 10), f(2, 5)],
2393            vec![f(0, 10), f(2, 5), f(3, 4), f(6, 9)],
2394            vec![f(0, 4), f(0, 4)],
2395        ];
2396        for folds in cases {
2397            for level in 0..=6u32 {
2398                let mut via_early_out = folds.clone();
2399                apply_fold_level(&mut via_early_out, level);
2400
2401                let levels = fold_levels(&folds);
2402                let mut expected = folds.clone();
2403                for (fold, depth) in expected.iter_mut().zip(levels) {
2404                    fold.closed = depth > level;
2405                }
2406
2407                assert_eq!(
2408                    via_early_out.iter().map(|x| x.closed).collect::<Vec<_>>(),
2409                    expected.iter().map(|x| x.closed).collect::<Vec<_>>(),
2410                    "level {level} over {} folds",
2411                    folds.len()
2412                );
2413            }
2414        }
2415    }
2416}