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}