hjkl_buffer/folds.rs
1//! Manual folds: contiguous row ranges that the host can collapse
2//! to a single visible "fold marker" line.
3//!
4//! Phase 9 of the migration plan unlocks this — vim users get
5//! `zo`/`zc`/`za`/`zR`/`zM` over the same buffer the editor is
6//! mutating, no separate fold tracker required.
7//!
8//! ## Fold semantics
9//!
10//! Folds are **row-range** spans, not byte spans. [`Fold`] covers
11//! `[start_row, end_row]` inclusive. The host renders folds as collapsed
12//! single-line stubs; the buffer never elides them on its own —
13//! [`crate::View::lines`] always returns the underlying logical text.
14//!
15//! Add / remove / toggle goes through
16//! [`crate::View::add_fold`] / [`crate::View::remove_fold_at`] /
17//! [`crate::View::toggle_fold_at`]. Open-all / close-all (`zR` / `zM`)
18//! go through [`crate::View::open_all_folds`] /
19//! [`crate::View::close_all_folds`]; folds keep their definitions across
20//! open/close cycles.
21
22/// A contiguous range of rows that the host can collapse to a single
23/// fold-marker line.
24///
25/// Folds are row-range spans: `[start_row, end_row]` inclusive. The buffer
26/// never elides content — [`crate::View::lines`] always returns the full
27/// logical text regardless of fold state. It is the host's render path that
28/// skips hidden rows and replaces them with a stub.
29///
30/// See the `folds` module documentation for the full invariant description.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Fold {
33 /// First row of the folded range (visible when closed).
34 pub start_row: usize,
35 /// Last row of the folded range, inclusive.
36 pub end_row: usize,
37 /// `true` = collapsed (rows after `start_row` are hidden).
38 pub closed: bool,
39 /// `true` when this fold was created by the auto-fold engine
40 /// (tree-sitter foldmethod=expr). Manual folds created via `zf` /
41 /// [`crate::View::add_fold`] set this to `false`.
42 ///
43 /// Used by [`crate::View::set_auto_folds`] to distinguish auto
44 /// folds (which it manages) from manual folds (which it leaves
45 /// untouched).
46 pub auto_generated: bool,
47}
48
49impl Fold {
50 pub fn contains(&self, row: usize) -> bool {
51 row >= self.start_row && row <= self.end_row
52 }
53
54 /// True when `row` is hidden by a closed fold (i.e. inside the
55 /// fold but not on its `start_row` marker line).
56 pub fn hides(&self, row: usize) -> bool {
57 self.closed && row > self.start_row && row <= self.end_row
58 }
59
60 /// Number of rows the fold spans.
61 pub fn line_count(&self) -> usize {
62 self.end_row.saturating_sub(self.start_row) + 1
63 }
64}
65
66/// Sorted in canonical `(start_row ASC, end_row DESC)` order over a fold slice
67/// for O(log F) "is this row hidden by a closed fold" queries.
68///
69/// The O(log F) twin of `folds.iter().any(|f| f.hides(row))`. Closed
70/// folds' `(start_row, end_row]` hidden ranges are merged into a disjoint
71/// union, and one `partition_point` over the merged ranges answers the
72/// query. Merging (not just sorting) is what keeps this correct for
73/// nested folds: a fold that starts inside an enclosing one can end
74/// before it while the enclosing fold still covers the query row, so
75/// "the last fold with `start_row <= row`" alone is not enough — that
76/// fold may have already ended while an earlier, longer one still hides
77/// the row.
78///
79/// Mirrors the TUI renderer's `FoldIndex` (`hjkl-buffer-tui`
80/// render.rs) — the same merge, the same query, the same answers. That
81/// copy sorts its input defensively because its fold source is not
82/// guaranteed ordered; this one relies on the buffer's canonical
83/// `(start_row ASC, end_row DESC)` ordering invariant (see
84/// [`crate::View::add_fold`] and [`crate::View::set_auto_folds`]) and skips the
85/// sort so the build stays O(F) — it runs per keystroke / per frame on hot
86/// paths. Debug builds assert the invariant, so feeding unsorted folds fails
87/// loudly in tests rather than silently mis-answering in release.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct FoldIndex {
90 /// Merged, disjoint half-open intervals `(start, end]` covering rows
91 /// hidden by at least one closed fold, sorted by `start`. A row `r` is
92 /// hidden iff `start < r <= end` for the interval with the greatest
93 /// `start <= r`.
94 hidden_ranges: Vec<(usize, usize)>,
95}
96
97impl FoldIndex {
98 /// Build the merged hidden-range index from a fold slice in canonical
99 /// `(start_row ASC, end_row DESC)` order (the buffer invariant — see the
100 /// type docs).
101 ///
102 /// O(F): one filter pass over the folds, then a single merge pass.
103 pub fn new(folds: &[Fold]) -> Self {
104 debug_assert!(
105 folds
106 .windows(2)
107 .all(|window| compare_folds(&window[0], &window[1]).is_le()),
108 "FoldIndex::new requires folds in canonical order"
109 );
110 // Merge the closed folds' (start, end] intervals into a disjoint
111 // union. `hides` membership is "in the union", so a single binary
112 // search over the merged ranges is correct even with nesting.
113 let mut hidden_ranges: Vec<(usize, usize)> = Vec::with_capacity(folds.len());
114 for f in folds.iter().filter(|f| f.closed) {
115 let (s, e) = (f.start_row, f.end_row);
116 match hidden_ranges.last_mut() {
117 // Overlaps the previous interval (or abuts it exactly —
118 // `s <= last_end` leaves no uncovered row between them) →
119 // extend. A gap needs `s > last_end`, e.g. (0,5] then (7,9]:
120 // row 6 is covered by neither, so they stay separate.
121 Some((_, last_end)) if s <= *last_end => {
122 *last_end = (*last_end).max(e);
123 }
124 _ => hidden_ranges.push((s, e)),
125 }
126 }
127 Self { hidden_ranges }
128 }
129
130 /// True when `row` is hidden by a closed fold — the O(log F) twin of
131 /// `folds.iter().any(|f| f.hides(row))`.
132 pub fn hides_row(&self, row: usize) -> bool {
133 let idx = self.hidden_ranges.partition_point(|&(s, _)| s <= row);
134 if idx == 0 {
135 return false;
136 }
137 let (s, e) = self.hidden_ranges[idx - 1];
138 row > s && row <= e
139 }
140}
141
142/// Compare folds in canonical order: earliest start first, then widest range
143/// first when ranges share a start row.
144fn compare_folds(left: &Fold, right: &Fold) -> std::cmp::Ordering {
145 left.start_row
146 .cmp(&right.start_row)
147 .then_with(|| right.end_row.cmp(&left.end_row))
148}
149
150/// Index of the deepest fold containing `row`: latest start first, then the
151/// shortest range for folds that share a start row.
152fn innermost_containing_fold_index(folds: &[Fold], row: usize) -> Option<usize> {
153 folds
154 .iter()
155 .enumerate()
156 .filter(|(_, fold)| fold.contains(row))
157 .max_by_key(|(_, fold)| (fold.start_row, std::cmp::Reverse(fold.end_row)))
158 .map(|(index, _)| index)
159}
160
161/// Index of the recursive fold target at `row`. A fold that starts on the
162/// cursor row takes precedence; among same-start folds, that is the widest
163/// range so `zC` / `zO` / `zA` include the complete same-start nesting.
164/// Otherwise, use the innermost containing fold, matching the single-fold
165/// commands' target selection.
166fn recursive_fold_target_index(folds: &[Fold], row: usize) -> Option<usize> {
167 folds
168 .iter()
169 .position(|fold| fold.start_row == row)
170 .or_else(|| innermost_containing_fold_index(folds, row))
171}
172
173impl crate::View {
174 /// Returns a snapshot of all folds as an owned `Vec<Fold>`.
175 ///
176 /// Owned rather than `&[Fold]` because a `View` is a per-window
177 /// view onto a shared `Buffer`; another view could mutate the folds vec
178 /// between when this returns and when the caller reads the slice.
179 pub fn folds(&self) -> Vec<Fold> {
180 self.content_lock().folds.clone()
181 }
182
183 /// Run `f` against the fold list under a **single** content lock, with
184 /// no clone. The borrow-style twin of [`Self::folds`] — prefer it for
185 /// every read-only query (`hides` scans, `is_empty`, per-row loops);
186 /// keep [`Self::folds`] only where an owned snapshot must outlive the
187 /// lock (e.g. it is stored, or the buffer is re-borrowed mutably).
188 ///
189 /// The closure runs with the content mutex held: it must not call back
190 /// into any `&self` method of this `View` (they all re-lock, and the
191 /// mutex is not re-entrant). Hoist such reads — `row_count()`,
192 /// `cursor()`, … — above the call.
193 pub fn with_folds<T>(&self, f: impl FnOnce(&[Fold]) -> T) -> T {
194 f(&self.content_lock().folds)
195 }
196
197 /// True when at least one fold is defined (open or closed). One lock,
198 /// no clone — the cheap form of `!folds().is_empty()`.
199 pub fn has_folds(&self) -> bool {
200 !self.content_lock().folds.is_empty()
201 }
202
203 /// Monotonic fold-mutation generation. Bumps only when a fold mutator
204 /// actually changes the fold set; read-only queries and plain text
205 /// edits leave it alone. Hosts caching a fold snapshot (`hjkl`'s
206 /// per-window `window_folds`) compare this instead of re-cloning the
207 /// fold `Vec` every keystroke.
208 ///
209 /// Conservative in the same sense as [`Self::dirty_gen`]: "if it
210 /// changed, the folds **may** have changed" (a `rebase_folds` whose
211 /// row-shift happens to move nothing still bumps).
212 pub fn fold_gen(&self) -> u64 {
213 self.content_lock().fold_gen
214 }
215
216 /// Record a fold-set mutation: bump the fold generation *and* the
217 /// render-cache generation (a fold change repaints). Every mutator in
218 /// this module funnels through here.
219 fn folds_changed(&mut self) {
220 self.fold_gen_bump();
221 self.dirty_gen_bump();
222 }
223
224 /// Register a new fold. An existing fold with the exact same inclusive
225 /// range is replaced; distinct ranges sharing a start row are retained.
226 /// Folds stay in canonical `(start_row ASC, end_row DESC)` order. Empty /
227 /// inverted ranges are rejected.
228 pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
229 if end_row < start_row {
230 return;
231 }
232 let last = self.row_count().saturating_sub(1);
233 if start_row > last {
234 return;
235 }
236 let end_row = end_row.min(last);
237 let fold = Fold {
238 start_row,
239 end_row,
240 closed,
241 auto_generated: false,
242 };
243 {
244 let mut c = self.content_lock_mut();
245 if let Some(idx) = c
246 .folds
247 .iter()
248 .position(|f| (f.start_row, f.end_row) == (start_row, end_row))
249 {
250 c.folds[idx] = fold;
251 } else {
252 let pos = c
253 .folds
254 .partition_point(|existing| compare_folds(existing, &fold).is_lt());
255 c.folds.insert(pos, fold);
256 }
257 }
258 self.folds_changed();
259 }
260
261 /// Replace all auto-generated folds with a new set derived from
262 /// `ranges`, while leaving manual folds untouched.
263 ///
264 /// ## `foldlevelstart`, not a boolean
265 ///
266 /// The second parameter is vim's `'foldlevelstart'` verbatim, and the rule
267 /// it implements is vim's: **a fold whose (1-based) nesting level is
268 /// greater than `foldlevelstart` starts closed**, everything at or above
269 /// that level starts open. So `0` closes everything, `1` leaves top-level
270 /// folds open and closes what is inside them, `99` (hjkl's default) opens
271 /// everything.
272 ///
273 /// It takes the raw option rather than a `default_closed: bool` — the
274 /// shape it replaced, which could only express "all closed" / "all open" —
275 /// and rather than a per-range level supplied by the caller, because:
276 ///
277 /// - **Nesting level is derivable from the ranges alone** (a fold's level
278 /// is one more than the number of other ranges that strictly contain
279 /// it), so a caller passing levels would be passing information this
280 /// function can compute — and would have to recompute identically at
281 /// every call site, marker scan and tree-sitter query alike.
282 /// - **Only this function knows the final set.** Levels have to be counted
283 /// over the ranges that actually become folds, and that is decided
284 /// *here*: single-row, inverted and out-of-range entries are dropped,
285 /// `end_row` is clamped, duplicate start rows collapse, and rows owned
286 /// by a manual fold are skipped. A level computed by the caller over the
287 /// raw ranges would be counting folds that do not exist.
288 ///
289 /// Levels are counted over the **auto** folds only: a `zf` fold enclosing
290 /// an auto one is the user's own structure, and letting it push the auto
291 /// fold a level deeper would make the auto fold's start state depend on
292 /// unrelated manual folding.
293 ///
294 /// vim's own default for the option is `-1`, meaning "do nothing, leave
295 /// `'foldlevel'` alone" (which, with `'foldlevel'` at its own default of
296 /// `0`, ends up closing everything). hjkl's `foldlevelstart` is a `u32`
297 /// defaulting to `99`, so that mode does not exist here and no value is
298 /// reserved for it — every value is a real level.
299 ///
300 /// ## Algorithm (O(N log N) — bounded by `ranges.len()`, no unbounded growth)
301 ///
302 /// 1. Snapshot `(start_row, end_row) → closed` for every existing auto fold
303 /// so each range's open/closed state survives a reparse.
304 /// 2. Retain only manual folds (`auto_generated == false`).
305 /// 3. Normalise `ranges` into the set that will actually become folds.
306 /// 4. Assign each of those a nesting level by a containment sweep.
307 /// 5. Insert one new `Fold` per surviving range, re-using the snapshotted
308 /// closed state when its exact range existed before, else
309 /// `level > foldlevelstart`.
310 ///
311 /// Invariants preserved:
312 /// - Folds stay in canonical `(start_row ASC, end_row DESC)` order.
313 /// - Duplicate exact ranges are deduplicated deterministically. Distinct
314 /// ranges sharing a start row are retained.
315 /// - Empty / inverted ranges (end_row < start_row) are silently skipped.
316 /// - `end_row` is clamped to the last valid row, same as `add_fold`.
317 /// - A MANUAL fold's start_row is never taken over: the auto range for
318 /// that row is dropped instead. `zf` is an explicit choice of extent,
319 /// and converting it to an auto fold both changed it and handed it to
320 /// the auto engine to overwrite on the next reparse.
321 /// - An auto fold with an existing exact range keeps its open / closed
322 /// state: `foldlevelstart` decides how a fold *starts*, so it applies to
323 /// newly-appearing ranges only and never re-closes a fold the user opened.
324 /// - When the resulting fold set is identical to the current one, nothing
325 /// is written and NO generation bumps — [`Self::fold_gen`] promises to
326 /// move only on a real change, and `dirty_gen` moving every pass made
327 /// the caller's "recompute once per edit" guard fire every frame (and
328 /// with it a full re-highlight, since `dirty_gen` keys that cache).
329 pub fn set_auto_folds(&mut self, ranges: &[(usize, usize)], foldlevelstart: u32) {
330 // 1. Snapshot closed state of existing auto folds by exact range.
331 let prev_closed: std::collections::HashMap<(usize, usize), bool> = self
332 .content_lock()
333 .folds
334 .iter()
335 .filter(|f| f.auto_generated)
336 .map(|f| ((f.start_row, f.end_row), f.closed))
337 .collect();
338
339 // 2. Start from the manual folds — they survive untouched, and their
340 // start rows are off-limits to the ranges below.
341 let mut next: Vec<Fold> = self
342 .content_lock()
343 .folds
344 .iter()
345 .filter(|f| !f.auto_generated)
346 .copied()
347 .collect();
348 let manual_starts: std::collections::HashSet<usize> =
349 next.iter().map(|f| f.start_row).collect();
350
351 // 3. Normalise: drop what will never become a fold, then sort and
352 // deduplicate exact ranges so the level sweep counts only real folds.
353 let last = self.row_count().saturating_sub(1);
354 let mut accepted: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
355 for &(start_row, end_row) in ranges {
356 // Skip empty/inverted and out-of-bounds ranges.
357 if end_row < start_row || start_row > last {
358 continue;
359 }
360 let end_row = end_row.min(last);
361 // Only folds spanning more than one row are meaningful.
362 if end_row == start_row || manual_starts.contains(&start_row) {
363 continue;
364 }
365 accepted.push((start_row, end_row));
366 }
367 accepted
368 .sort_unstable_by_key(|&(start_row, end_row)| (start_row, std::cmp::Reverse(end_row)));
369 accepted.dedup();
370
371 // 4. Nesting level per accepted range, by a single containment sweep.
372 // Visiting outermost-first makes `stack` the chain of enclosing
373 // folds: pop everything that ends before this range does — that
374 // covers both a sibling that already closed and a partial overlap,
375 // neither of which encloses it — and what is left is exactly the
376 // enclosing chain.
377 let mut levels = Vec::with_capacity(accepted.len());
378 let mut stack: Vec<usize> = Vec::new();
379 for &(_, end_row) in &accepted {
380 while stack.last().is_some_and(|&open_end| open_end < end_row) {
381 stack.pop();
382 }
383 // 1-based, matching vim: an unnested fold is level 1.
384 levels.push(stack.len() as u32 + 1);
385 stack.push(end_row);
386 }
387
388 // 5. Insert the new auto folds, then restore canonical ordering.
389 next.extend(
390 accepted
391 .iter()
392 .zip(levels)
393 .map(|(&(start_row, end_row), level)| Fold {
394 start_row,
395 end_row,
396 closed: prev_closed
397 .get(&(start_row, end_row))
398 .copied()
399 .unwrap_or(level > foldlevelstart),
400 auto_generated: true,
401 }),
402 );
403 next.sort_by(compare_folds);
404
405 // 6. No-op when nothing actually changed — see the invariant above.
406 if self.content_lock().folds == next {
407 return;
408 }
409 self.content_lock_mut().folds = next;
410 self.folds_changed();
411 }
412
413 /// Drop the fold whose range covers `row`. Returns `true` when a
414 /// fold was actually removed.
415 pub fn remove_fold_at(&mut self, row: usize) -> bool {
416 // Remove the innermost fold containing `row`, so `zd` on a nested
417 // fold drops the inner one, not the enclosing block.
418 let idx = innermost_containing_fold_index(&self.content_lock().folds, row);
419 let Some(idx) = idx else {
420 return false;
421 };
422 self.content_lock_mut().folds.remove(idx);
423 self.folds_changed();
424 true
425 }
426
427 /// Open the fold at `row` (no-op if already open or no fold).
428 pub fn open_fold_at(&mut self, row: usize) -> bool {
429 let Some(idx) = innermost_containing_fold_index(&self.content_lock().folds, row) else {
430 return false;
431 };
432 let changed = {
433 let mut c = self.content_lock_mut();
434 let f = &mut c.folds[idx];
435 if !f.closed {
436 return false;
437 }
438 f.closed = false;
439 true
440 };
441 if changed {
442 self.folds_changed();
443 }
444 changed
445 }
446
447 /// Close the fold at `row` (no-op if already closed or no fold).
448 pub fn close_fold_at(&mut self, row: usize) -> bool {
449 let Some(idx) = innermost_containing_fold_index(&self.content_lock().folds, row) else {
450 return false;
451 };
452 let changed = {
453 let mut c = self.content_lock_mut();
454 let f = &mut c.folds[idx];
455 if f.closed {
456 return false;
457 }
458 f.closed = true;
459 true
460 };
461 if changed {
462 self.folds_changed();
463 }
464 changed
465 }
466
467 /// Flip the closed/open state of the fold containing `row`.
468 pub fn toggle_fold_at(&mut self, row: usize) -> bool {
469 let Some(idx) = innermost_containing_fold_index(&self.content_lock().folds, row) else {
470 return false;
471 };
472 let changed = {
473 let mut c = self.content_lock_mut();
474 let f = &mut c.folds[idx];
475 f.closed = !f.closed;
476 true
477 };
478 if changed {
479 self.folds_changed();
480 }
481 changed
482 }
483
484 /// `zC` — close the fold subtree at `row`. Folds that start on `row`
485 /// select their widest range, so same-start nested folds close together;
486 /// otherwise the innermost containing fold and its descendants close.
487 pub fn close_folds_recursively_at(&mut self, row: usize) -> bool {
488 let changed = {
489 let mut c = self.content_lock_mut();
490 let Some(target) = recursive_fold_target_index(&c.folds, row).map(|idx| c.folds[idx])
491 else {
492 return false;
493 };
494 let mut any = false;
495 for fold in &mut c.folds {
496 if fold.start_row >= target.start_row
497 && fold.end_row <= target.end_row
498 && !fold.closed
499 {
500 fold.closed = true;
501 any = true;
502 }
503 }
504 any
505 };
506 if changed {
507 self.folds_changed();
508 }
509 changed
510 }
511
512 /// `zO` — open the fold subtree at `row`. Target selection matches
513 /// [`Self::close_folds_recursively_at`].
514 pub fn open_folds_recursively_at(&mut self, row: usize) -> bool {
515 let changed = {
516 let mut c = self.content_lock_mut();
517 let Some(target) = recursive_fold_target_index(&c.folds, row).map(|idx| c.folds[idx])
518 else {
519 return false;
520 };
521 let mut any = false;
522 for fold in &mut c.folds {
523 if fold.start_row >= target.start_row
524 && fold.end_row <= target.end_row
525 && fold.closed
526 {
527 fold.closed = false;
528 any = true;
529 }
530 }
531 any
532 };
533 if changed {
534 self.folds_changed();
535 }
536 changed
537 }
538
539 /// `zA` — toggle the fold subtree at `row`. If every fold in the subtree
540 /// is open, close it; otherwise open the entire subtree.
541 pub fn toggle_folds_recursively_at(&mut self, row: usize) -> bool {
542 let changed = {
543 let mut c = self.content_lock_mut();
544 let Some(target) = recursive_fold_target_index(&c.folds, row).map(|idx| c.folds[idx])
545 else {
546 return false;
547 };
548 let closed = !c.folds.iter().any(|fold| {
549 fold.start_row >= target.start_row && fold.end_row <= target.end_row && fold.closed
550 });
551 for fold in &mut c.folds {
552 if fold.start_row >= target.start_row && fold.end_row <= target.end_row {
553 fold.closed = closed;
554 }
555 }
556 true
557 };
558 if changed {
559 self.folds_changed();
560 }
561 changed
562 }
563
564 /// `zR` — open every fold.
565 pub fn open_all_folds(&mut self) {
566 let changed = {
567 let mut c = self.content_lock_mut();
568 let mut any = false;
569 for f in c.folds.iter_mut() {
570 if f.closed {
571 f.closed = false;
572 any = true;
573 }
574 }
575 any
576 };
577 if changed {
578 self.folds_changed();
579 }
580 }
581
582 /// `zE` — eliminate every fold.
583 pub fn clear_all_folds(&mut self) {
584 let was_nonempty = !self.content_lock().folds.is_empty();
585 if was_nonempty {
586 self.content_lock_mut().folds.clear();
587 self.folds_changed();
588 }
589 }
590
591 /// `zM` — close every fold.
592 pub fn close_all_folds(&mut self) {
593 let changed = {
594 let mut c = self.content_lock_mut();
595 let mut any = false;
596 for f in c.folds.iter_mut() {
597 if !f.closed {
598 f.closed = true;
599 any = true;
600 }
601 }
602 any
603 };
604 if changed {
605 self.folds_changed();
606 }
607 }
608
609 /// Deepest fold whose range contains `row`. Useful for the host's
610 /// `za`/`zo`/`zc` handlers. Ranges sharing a start row select the shortest
611 /// one.
612 pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
613 let folds = self.content_lock();
614 innermost_containing_fold_index(&folds.folds, row).map(|idx| folds.folds[idx])
615 }
616
617 /// True iff `row` is hidden by a closed fold (any fold).
618 pub fn is_row_hidden(&self, row: usize) -> bool {
619 self.with_folds(|folds| folds.iter().any(|f| f.hides(row)))
620 }
621
622 /// Open every closed fold whose body hides `row`, so the row becomes
623 /// visible. Handles nested folds in a single pass — unlike
624 /// `open_fold_at` / `FoldOp::OpenAt`, which only act on the first fold
625 /// containing the row and so can never reach a nested inner fold.
626 /// Used by `goto_line` so a jump into a folded region reveals the
627 /// target line instead of stranding the cursor on a hidden row.
628 /// Returns `true` if any fold was opened.
629 pub fn reveal_row(&mut self, row: usize) -> bool {
630 let changed = {
631 let mut c = self.content_lock_mut();
632 let mut any = false;
633 for f in c.folds.iter_mut() {
634 if f.hides(row) {
635 f.closed = false;
636 any = true;
637 }
638 }
639 any
640 };
641 if changed {
642 self.folds_changed();
643 }
644 changed
645 }
646
647 /// Last row index containing real content — skips vim's single
648 /// phantom trailing empty row. `ropey`'s `len_lines()` always
649 /// synthesizes one extra empty final "line" when the buffer text
650 /// ends in `\n` (vim treats that `\n` as a terminator, not a
651 /// separator). Mirrors `hjkl_engine::motions::move_bottom`'s clamp
652 /// (`G`) so vertical motions agree with `G` on where the buffer
653 /// "ends". A buffer whose *real* last line happens to be empty
654 /// (e.g. `"foo\n\n"`, row 1) is untouched — only a single trailing
655 /// phantom row is ever skipped.
656 ///
657 /// The emptiness test reads the last row's byte length rather than
658 /// materializing the row as a `String`: for the final rope line the two
659 /// agree exactly (ropey never gives the last line a trailing `\n`, so
660 /// `rope_line_str` has nothing to strip and
661 /// `rope_line_bytes(last) == 0` iff `rope_line_str(last).is_empty()`).
662 pub fn last_content_row(&self) -> usize {
663 let raw_last = self.row_count().saturating_sub(1);
664 if raw_last > 0 {
665 let c = self.content_lock();
666 if crate::buffer::rope_line_bytes(&c.text, raw_last) == 0 {
667 return raw_last - 1;
668 }
669 }
670 raw_last
671 }
672
673 /// First visible row strictly after `row`, skipping any rows hidden
674 /// by closed folds. Returns `None` past the end of the buffer.
675 ///
676 /// Takes the content lock **once** for the whole walk (via
677 /// [`Self::with_folds`]) instead of once per skipped row — `j` over a
678 /// long closed fold used to pay a lock + full `Vec<Fold>` clone per row.
679 /// `last_content_row()` locks too, so it is resolved before the scan.
680 pub fn next_visible_row(&self, row: usize) -> Option<usize> {
681 let last = self.last_content_row();
682 if last == 0 && row == 0 {
683 return None;
684 }
685 let mut r = row.checked_add(1)?;
686 self.with_folds(|folds| {
687 let index = FoldIndex::new(folds);
688 while r <= last && index.hides_row(r) {
689 r += 1;
690 }
691 (r <= last).then_some(r)
692 })
693 }
694
695 /// First visible row strictly before `row`, skipping hidden rows.
696 ///
697 /// One lock for the whole walk, same as [`Self::next_visible_row`].
698 pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
699 let mut r = row.checked_sub(1)?;
700 self.with_folds(|folds| {
701 let index = FoldIndex::new(folds);
702 while index.hides_row(r) {
703 r = r.checked_sub(1)?;
704 }
705 Some(r)
706 })
707 }
708
709 /// Drop every fold that touches `[start_row, end_row]`.
710 pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
711 let before = self.content_lock().folds.len();
712 invalidate_folds(&mut self.content_lock_mut().folds, start_row, end_row);
713 if self.content_lock().folds.len() != before {
714 self.folds_changed();
715 }
716 }
717
718 /// Shift every buffer fold by an edit's row-delta band. Mirrors
719 /// [`crate::buffer::View::rebase_marks`] for the shared fold storage —
720 /// see [`shift_folds_after_edit`] for the per-fold rules.
721 pub fn rebase_folds(
722 &mut self,
723 edit_start: usize,
724 drop_end: usize,
725 shift_threshold: usize,
726 delta: isize,
727 ) {
728 if delta == 0 {
729 return;
730 }
731 let touched = {
732 let mut c = self.content_lock_mut();
733 if c.folds.is_empty() {
734 false
735 } else {
736 shift_folds_after_edit(&mut c.folds, edit_start, drop_end, shift_threshold, delta);
737 true
738 }
739 };
740 if touched {
741 // Conservative: a shift that happened to move nothing (every
742 // fold entirely below the edit) still bumps. Matches the
743 // `dirty_gen` contract — "may have changed".
744 self.fold_gen_bump();
745 }
746 }
747
748 /// Replace the entire fold set wholesale. Used to install a per-window fold
749 /// snapshot into the shared buffer on focus change (window-level folds): the
750 /// app keeps each window's open/closed state and swaps it in before dispatch,
751 /// so motions/render/`z`-ops operate on the focused window's folds. Input
752 /// is normalized to canonical `(start_row ASC, end_row DESC)` order, with
753 /// exact-range duplicates collapsed.
754 pub fn set_folds(&mut self, folds: &[Fold]) {
755 let mut next = folds.to_vec();
756 next.sort_by(compare_folds);
757 next.dedup_by_key(|fold| (fold.start_row, fold.end_row));
758 {
759 let mut c = self.content_lock_mut();
760 if c.folds == next {
761 return; // no-op — avoid a spurious dirty_gen bump
762 }
763 c.folds = next;
764 }
765 self.folds_changed();
766 }
767}
768
769/// Drop every fold in `folds` that touches `[start_row, end_row]`, in place.
770///
771/// Free helper so both [`crate::View::invalidate_folds_in_range`] (operating
772/// on the shared content) and the app's window-level edit-coherence pass
773/// (operating on a sibling window's owned `Vec<Fold>`) share one rule — vim
774/// opens/forgets any fold the edit overlapped.
775pub fn invalidate_folds(folds: &mut Vec<Fold>, start_row: usize, end_row: usize) {
776 folds.retain(|f| f.end_row < start_row || f.start_row > end_row);
777}
778
779// ── Row-delta shifting (edit-coherence) ──────────────────────────────────
780//
781// A manual (`zf`) fold is a row-range that has to track the same
782// insert/delete row-shift the engine already applies to marks and the
783// jumplist (see `Editor::shift_marks_after_edit`). Without this, a fold
784// below an edit keeps stale row numbers and the renderer / fold-aware ops
785// (`dd`, `p`, …) act on the wrong rows (#audit-r2 fix 1).
786//
787// The four `(edit_start, drop_end, shift_threshold, delta)` parameters are
788// the exact same band description `Editor::shift_marks_after_edit` computes
789// for marks: `[edit_start, drop_end)` is the row band the edit deleted
790// (empty for inserts), and any row `>= shift_threshold` moves by `delta`.
791// Reusing the identical band keeps folds, marks, and jumplist entries
792// shifting in lockstep for the same edit.
793
794/// Shift a single fold's `start_row` / `end_row` by an edit's row-delta band.
795/// Returns `None` when the edit's deleted band fully consumes the fold.
796///
797/// Each endpoint is mapped independently through the same drop/shift rule
798/// [`crate::buffer::View::rebase_marks`] applies to a point mark. Mapping
799/// the two endpoints independently is what produces the vim-shaped "edit
800/// inside a fold" semantics for free:
801/// - Both endpoints below `shift_threshold` and outside the deleted band →
802/// fold untouched (edit happened entirely outside the fold).
803/// - `start_row` outside the deleted band but `end_row` inside it → the
804/// edit deleted the fold's tail; it clips to end at the last surviving
805/// row (`edit_start - 1`).
806/// - `start_row` inside the deleted band but `end_row` outside it → the
807/// edit deleted the fold's head; it clips to start at `edit_start` (the
808/// row the surviving tail now occupies).
809/// - Both endpoints inside the deleted band → the edit consumed the whole
810/// fold; it's dropped.
811/// - `start_row` below the threshold and `end_row` at/above it (an insert
812/// or a deletion landing strictly inside the fold) → `start_row` stays,
813/// `end_row` shifts by `delta`: the fold grows (insert) or shrinks
814/// (delete) around the edit, matching vim.
815/// - Both endpoints at/above the threshold → the fold shifts wholesale.
816pub fn shift_fold(
817 fold: Fold,
818 edit_start: usize,
819 drop_end: usize,
820 shift_threshold: usize,
821 delta: isize,
822) -> Option<Fold> {
823 if delta == 0 {
824 return Some(fold);
825 }
826 let map_row = |row: usize| -> Option<usize> {
827 if (edit_start..drop_end).contains(&row) {
828 None
829 } else if row >= shift_threshold {
830 Some(((row as isize) + delta).max(0) as usize)
831 } else {
832 Some(row)
833 }
834 };
835 let mapped_start = map_row(fold.start_row);
836 let mapped_end = map_row(fold.end_row);
837 if mapped_start.is_none() && mapped_end.is_none() {
838 return None;
839 }
840 let new_start = mapped_start.unwrap_or(edit_start);
841 let new_end = mapped_end.unwrap_or_else(|| edit_start.saturating_sub(1));
842 if new_end < new_start {
843 return None;
844 }
845 Some(Fold {
846 start_row: new_start,
847 end_row: new_end,
848 closed: fold.closed,
849 auto_generated: fold.auto_generated,
850 })
851}
852
853/// Shift every fold in `folds` by an edit's row-delta band, in place.
854/// Folds the edit's deleted band fully consumes are dropped (mirrors
855/// [`invalidate_folds`] for the folds that DO survive but move). The surviving
856/// folds are normalized to canonical `(start_row ASC, end_row DESC)` order and
857/// exact-range collisions retain the first stable canonical fold.
858///
859/// Shared by [`crate::View::rebase_folds`] (engine-side, the buffer's own
860/// fold storage) and the app's sibling-window fold snapshot shift, so both
861/// converge on the identical row-shift rule.
862pub fn shift_folds_after_edit(
863 folds: &mut Vec<Fold>,
864 edit_start: usize,
865 drop_end: usize,
866 shift_threshold: usize,
867 delta: isize,
868) {
869 if delta == 0 {
870 return;
871 }
872 folds.retain_mut(
873 |f| match shift_fold(*f, edit_start, drop_end, shift_threshold, delta) {
874 Some(shifted) => {
875 *f = shifted;
876 true
877 }
878 None => false,
879 },
880 );
881 folds.sort_by(compare_folds);
882 folds.dedup_by_key(|fold| (fold.start_row, fold.end_row));
883}
884
885#[cfg(test)]
886mod tests {
887 use crate::View;
888
889 fn b() -> View {
890 View::from_str("a\nb\nc\nd\ne")
891 }
892
893 #[test]
894 fn add_keeps_folds_in_start_row_order() {
895 let mut buf = b();
896 buf.add_fold(2, 3, true);
897 buf.add_fold(0, 1, false);
898 let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
899 assert_eq!(starts, vec![0, 2]);
900 }
901
902 #[test]
903 fn set_folds_replaces_wholesale() {
904 let mut buf = b();
905 buf.add_fold(0, 1, false);
906 // Install a different per-window snapshot.
907 let snapshot = vec![super::Fold {
908 start_row: 2,
909 end_row: 3,
910 closed: true,
911 auto_generated: false,
912 }];
913 buf.set_folds(&snapshot);
914 assert_eq!(buf.folds(), snapshot);
915 // Idempotent: re-installing the same set is a no-op (no dirty bump).
916 let dg = buf.dirty_gen();
917 buf.set_folds(&snapshot);
918 assert_eq!(buf.dirty_gen(), dg);
919 }
920
921 #[test]
922 fn invalidate_folds_helper_drops_overlapping() {
923 let f = |s, e| super::Fold {
924 start_row: s,
925 end_row: e,
926 closed: true,
927 auto_generated: false,
928 };
929 let mut folds = vec![f(0, 2), f(4, 6), f(8, 10)];
930 // Edit touches rows 5..5 → only the [4,6] fold overlaps.
931 super::invalidate_folds(&mut folds, 5, 5);
932 let starts: Vec<usize> = folds.iter().map(|x| x.start_row).collect();
933 assert_eq!(starts, vec![0, 8]);
934 }
935
936 #[test]
937 fn add_fold_retains_same_start_ranges_and_replaces_exact_duplicate() {
938 let mut buf = b();
939 buf.add_fold(1, 4, false);
940 buf.add_fold(1, 2, false);
941 buf.add_fold(1, 2, true);
942
943 assert_eq!(
944 buf.folds()
945 .iter()
946 .map(|f| (f.start_row, f.end_row, f.closed))
947 .collect::<Vec<_>>(),
948 vec![(1, 4, false), (1, 2, true)]
949 );
950 }
951
952 #[test]
953 fn add_clamps_end_row_to_buffer_bounds() {
954 let mut buf = b();
955 buf.add_fold(2, 99, true);
956 assert_eq!(buf.folds()[0].end_row, 4);
957 }
958
959 #[test]
960 fn add_rejects_inverted_range() {
961 let mut buf = b();
962 buf.add_fold(3, 1, true);
963 assert!(buf.folds().is_empty());
964 }
965
966 #[test]
967 fn toggle_flips_state() {
968 let mut buf = b();
969 buf.add_fold(1, 3, false);
970 assert!(!buf.folds()[0].closed);
971 assert!(buf.toggle_fold_at(2));
972 assert!(buf.folds()[0].closed);
973 assert!(buf.toggle_fold_at(2));
974 assert!(!buf.folds()[0].closed);
975 }
976
977 #[test]
978 fn same_start_selectors_target_the_innermost_fold() {
979 let mut buf = b();
980 buf.add_fold(0, 4, false);
981 buf.add_fold(0, 2, true);
982 assert!(buf.open_fold_at(1));
983 assert!(!buf.is_row_hidden(1));
984 assert!(!buf.is_row_hidden(2));
985 assert!(!buf.is_row_hidden(3));
986 assert!(!buf.is_row_hidden(4));
987
988 assert!(buf.toggle_fold_at(1));
989 assert!(buf.is_row_hidden(1));
990 assert!(buf.is_row_hidden(2));
991 assert!(!buf.is_row_hidden(3));
992 assert!(!buf.is_row_hidden(4));
993 assert_eq!(buf.fold_at_row(1).map(|fold| fold.end_row), Some(2));
994
995 assert!(buf.remove_fold_at(1));
996 assert!(!buf.is_row_hidden(1));
997 assert!(!buf.is_row_hidden(2));
998 assert!(!buf.is_row_hidden(3));
999 assert!(!buf.is_row_hidden(4));
1000 }
1001
1002 #[test]
1003 fn recursive_fold_commands_apply_to_only_the_target_subtree() {
1004 let mut buf = View::from_str("0\n1\n2\n3\n4\n5\n6\n7\n8");
1005 buf.add_fold(0, 6, false);
1006 buf.add_fold(2, 4, false);
1007 buf.add_fold(3, 3, false);
1008 buf.add_fold(5, 6, false);
1009
1010 assert!(buf.close_folds_recursively_at(2));
1011 assert_eq!(
1012 buf.folds()
1013 .iter()
1014 .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1015 .collect::<Vec<_>>(),
1016 vec![(0, 6, false), (2, 4, true), (3, 3, true), (5, 6, false)]
1017 );
1018 assert_eq!(buf.next_visible_row(2), Some(5));
1019
1020 assert!(buf.open_folds_recursively_at(2));
1021 assert!(buf.folds().iter().all(|fold| !fold.closed));
1022 }
1023
1024 #[test]
1025 fn recursive_toggle_uses_the_same_start_subtree_state() {
1026 let mut buf = View::from_str("a\nb\nc\nd\ne\nf");
1027 buf.add_fold(0, 4, false);
1028 buf.add_fold(0, 2, false);
1029
1030 assert!(buf.toggle_folds_recursively_at(0));
1031 assert_eq!(
1032 buf.folds()
1033 .iter()
1034 .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1035 .collect::<Vec<_>>(),
1036 vec![(0, 4, true), (0, 2, true)]
1037 );
1038 assert!((1..=4).all(|row| buf.is_row_hidden(row)));
1039 assert_eq!(buf.next_visible_row(0), Some(5));
1040
1041 assert!(buf.toggle_folds_recursively_at(0));
1042 assert_eq!(
1043 buf.folds()
1044 .iter()
1045 .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1046 .collect::<Vec<_>>(),
1047 vec![(0, 4, false), (0, 2, false)]
1048 );
1049 assert!((0..5).all(|row| !buf.is_row_hidden(row)));
1050
1051 assert!(buf.close_fold_at(0));
1052 assert_eq!(
1053 buf.folds()
1054 .iter()
1055 .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1056 .collect::<Vec<_>>(),
1057 vec![(0, 4, false), (0, 2, true)]
1058 );
1059 assert!(buf.is_row_hidden(1));
1060 assert!(buf.is_row_hidden(2));
1061 assert!(!buf.is_row_hidden(3));
1062 assert!(!buf.is_row_hidden(4));
1063 assert_eq!(buf.next_visible_row(0), Some(3));
1064
1065 assert!(buf.toggle_folds_recursively_at(0));
1066 assert_eq!(
1067 buf.folds()
1068 .iter()
1069 .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1070 .collect::<Vec<_>>(),
1071 vec![(0, 4, false), (0, 2, false)]
1072 );
1073 assert!((0..5).all(|row| !buf.is_row_hidden(row)));
1074 }
1075
1076 #[test]
1077 fn is_row_hidden_excludes_start_row() {
1078 let mut buf = b();
1079 buf.add_fold(1, 3, true);
1080 assert!(!buf.is_row_hidden(0));
1081 assert!(!buf.is_row_hidden(1)); // start row stays visible
1082 assert!(buf.is_row_hidden(2));
1083 assert!(buf.is_row_hidden(3));
1084 assert!(!buf.is_row_hidden(4));
1085 }
1086
1087 #[test]
1088 fn fold_index_hides_row_matches_naive_scan() {
1089 // The cases a naive "last fold with start_row <= row" binary search
1090 // gets wrong, pinned against the linear scan it replaces:
1091 // - a fold starting well before the queried row: row 99/100 are
1092 // covered by [0,100], but the last start_row <= 99/100 is [50,60];
1093 // - a nested fold ending before the enclosing one: row 7 is covered
1094 // by [1,10], but the last start_row <= 7 is [5,6] which ended at 6;
1095 // - row 10: covered by [1,10] and [0,100], while the last start_row
1096 // <= 10 is [8,9], which ends at 9.
1097 let f = |s, e, closed| super::Fold {
1098 start_row: s,
1099 end_row: e,
1100 closed,
1101 auto_generated: false,
1102 };
1103 let folds = vec![
1104 f(0, 100, true), // starts well before the queried rows
1105 f(1, 10, true), // encloses the nested folds below
1106 f(5, 6, true), // nested; ends before rows 7..9
1107 f(8, 9, true), // nested; covers rows 8..9
1108 f(20, 20, true), // degenerate: hides nothing itself
1109 f(30, 40, false), // open: hides nothing
1110 f(50, 60, true),
1111 ];
1112 let index = super::FoldIndex::new(&folds);
1113 for row in 0..200 {
1114 let naive = folds.iter().any(|f| f.hides(row));
1115 assert_eq!(index.hides_row(row), naive, "row {row}");
1116 }
1117 }
1118
1119 #[test]
1120 fn fold_index_agrees_with_buffer_folds_across_nested_and_open_mixes() {
1121 // Same guarantee through the real buffer API: `add_fold` keeps the
1122 // list in canonical order regardless of insertion order, and the
1123 // index must answer identically to the buffer's linear scan for a
1124 // nesting of closed/open/degenerate folds.
1125 let mut buf = View::from_str(&"x\n".repeat(60));
1126 buf.add_fold(0, 55, true);
1127 buf.add_fold(2, 3, true);
1128 buf.add_fold(5, 6, false); // open
1129 buf.add_fold(7, 7, true); // degenerate — hides nothing itself
1130 buf.add_fold(10, 20, true);
1131 buf.add_fold(15, 16, true); // nested inside [10,20]
1132 for row in 0..buf.row_count() {
1133 let naive = buf.folds().iter().any(|f| f.hides(row));
1134 let via_index = buf.with_folds(|folds| super::FoldIndex::new(folds).hides_row(row));
1135 assert_eq!(via_index, naive, "row {row}");
1136 }
1137 }
1138
1139 #[test]
1140 fn open_close_all_changes_every_fold() {
1141 let mut buf = b();
1142 buf.add_fold(0, 1, false);
1143 buf.add_fold(2, 3, true);
1144 buf.close_all_folds();
1145 assert!(buf.folds().iter().all(|f| f.closed));
1146 buf.open_all_folds();
1147 assert!(buf.folds().iter().all(|f| !f.closed));
1148 }
1149
1150 #[test]
1151 fn invalidate_drops_overlapping_folds() {
1152 let mut buf = b();
1153 buf.add_fold(0, 1, true);
1154 buf.add_fold(2, 3, true);
1155 buf.add_fold(4, 4, true);
1156 buf.invalidate_folds_in_range(2, 3);
1157 let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
1158 assert_eq!(starts, vec![0, 4]);
1159 }
1160
1161 // ── auto_generated flag + set_auto_folds ─────────────────────────────────
1162
1163 #[test]
1164 fn add_fold_sets_auto_generated_false() {
1165 let mut buf = b();
1166 buf.add_fold(1, 3, false);
1167 assert!(
1168 !buf.folds()[0].auto_generated,
1169 "manual add_fold must have auto_generated=false"
1170 );
1171 }
1172
1173 #[test]
1174 fn set_auto_folds_retains_same_start_nested_ranges() {
1175 let mut buf = b();
1176 buf.set_auto_folds(&[(0, 4), (0, 2)], 99);
1177
1178 assert_eq!(
1179 buf.folds()
1180 .iter()
1181 .map(|f| (f.start_row, f.end_row))
1182 .collect::<Vec<_>>(),
1183 vec![(0, 4), (0, 2)]
1184 );
1185 assert!(buf.close_fold_at(1));
1186 assert!(buf.is_row_hidden(1));
1187 assert!(buf.is_row_hidden(2));
1188 assert!(!buf.is_row_hidden(3));
1189 assert!(!buf.is_row_hidden(4));
1190 assert_eq!(buf.next_visible_row(0), Some(3));
1191 }
1192
1193 #[test]
1194 fn set_auto_folds_deduplicates_same_start_exact_ranges() {
1195 let mut buf = b();
1196 buf.set_auto_folds(&[(0, 4), (0, 2), (0, 4)], 99);
1197 assert_eq!(
1198 buf.folds()
1199 .iter()
1200 .map(|fold| (fold.start_row, fold.end_row))
1201 .collect::<Vec<_>>(),
1202 vec![(0, 4), (0, 2)]
1203 );
1204 }
1205
1206 #[test]
1207 fn set_auto_folds_adds_auto_folds() {
1208 let mut buf = b();
1209 buf.set_auto_folds(&[(0, 2), (3, 4)], 99);
1210 let folds = buf.folds();
1211 assert_eq!(folds.len(), 2);
1212 assert!(folds[0].auto_generated);
1213 assert!(folds[1].auto_generated);
1214 assert_eq!(folds[0].start_row, 0);
1215 assert_eq!(folds[1].start_row, 3);
1216 }
1217
1218 #[test]
1219 fn set_auto_folds_second_call_replaces_first() {
1220 let mut buf = b();
1221 buf.set_auto_folds(&[(0, 2), (3, 4)], 99);
1222 assert_eq!(buf.folds().len(), 2);
1223 // Replace with a different set.
1224 buf.set_auto_folds(&[(1, 4)], 99);
1225 let folds = buf.folds();
1226 assert_eq!(folds.len(), 1, "second call must replace first set");
1227 assert_eq!(folds[0].start_row, 1);
1228 assert!(folds[0].auto_generated);
1229 }
1230
1231 #[test]
1232 fn set_auto_folds_preserves_manual_folds() {
1233 let mut buf = b();
1234 // Add a manual fold.
1235 buf.add_fold(0, 1, true);
1236 // Auto-fold the remaining range.
1237 buf.set_auto_folds(&[(2, 4)], 99);
1238 let folds = buf.folds();
1239 assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
1240 let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
1241 assert!(!manual.auto_generated, "manual fold flag must stay false");
1242 let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
1243 assert!(auto.auto_generated);
1244 }
1245
1246 #[test]
1247 fn set_auto_folds_preserves_same_start_range_states_after_reordering() {
1248 let mut buf = b();
1249 buf.set_auto_folds(&[(0, 4), (0, 2)], 99);
1250 assert!(buf.close_fold_at(1));
1251
1252 buf.set_auto_folds(&[(0, 2), (0, 4)], 99);
1253 assert_eq!(
1254 buf.folds()
1255 .iter()
1256 .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1257 .collect::<Vec<_>>(),
1258 vec![(0, 4, false), (0, 2, true)]
1259 );
1260 }
1261
1262 #[test]
1263 fn set_auto_folds_preserves_open_closed_state_by_exact_range() {
1264 let mut buf = b();
1265 // First auto-fold pass: create a closed fold at row 0.
1266 buf.set_auto_folds(&[(0, 2)], 0); // foldlevelstart=0 → starts closed
1267 assert!(buf.folds()[0].closed, "fold must start closed per default");
1268
1269 // User opens the fold (simulated by toggle).
1270 buf.toggle_fold_at(0);
1271 assert!(!buf.folds()[0].closed, "fold must now be open");
1272
1273 // Second auto-fold pass with the same exact range preserves open state.
1274 buf.set_auto_folds(&[(0, 2)], 0); // foldlevelstart=0 but prev was open
1275 assert!(
1276 !buf.folds()[0].closed,
1277 "open/closed state must be preserved across set_auto_folds"
1278 );
1279 }
1280
1281 #[test]
1282 fn set_auto_folds_skips_single_row_and_inverted_ranges() {
1283 let mut buf = b();
1284 buf.set_auto_folds(&[(1, 1), (3, 2)], 99);
1285 assert!(
1286 buf.folds().is_empty(),
1287 "single-row and inverted ranges must be skipped"
1288 );
1289 }
1290
1291 #[test]
1292 fn set_auto_folds_new_folds_start_per_foldlevelstart() {
1293 let mut buf = b();
1294 buf.set_auto_folds(&[(0, 4)], 0);
1295 assert!(
1296 buf.folds()[0].closed,
1297 "new auto fold must start closed at foldlevelstart=0"
1298 );
1299
1300 // Re-running the same exact range at foldlevelstart=99 must NOT reopen
1301 // it: the snapshot preserves the state the fold already has, and
1302 // `foldlevelstart` only decides how a fold *starts*.
1303 buf.set_auto_folds(&[(0, 4)], 99);
1304 assert!(
1305 buf.folds()[0].closed,
1306 "an existing fold keeps its state — foldlevelstart is start-only"
1307 );
1308
1309 // A brand-new start row takes the option: level 1 <= 99 → open.
1310 let mut buf2 = b();
1311 buf2.set_auto_folds(&[(2, 4)], 99);
1312 assert!(
1313 !buf2.folds()[0].closed,
1314 "brand-new level-1 auto fold must start open at foldlevelstart=99"
1315 );
1316 }
1317
1318 // ── foldlevelstart: level semantics, not a boolean ────────────────────
1319 //
1320 // The expectations below are neovim 0.12.4's, measured rather than
1321 // reasoned: the same nesting opened with `foldmethod=expr` +
1322 // `v:lua.vim.treesitter.foldexpr()` and `foldlevelstart` set to each
1323 // value, with the closed folds enumerated via `foldclosed` /
1324 // `foldclosedend` (`foldlevel()` merges adjacent siblings and reads as a
1325 // false difference). Converted from vim's 1-based lines to 0-based rows.
1326 //
1327 // row source fold level
1328 // 2 function M.outer(a, b) 2..16 1
1329 // 3 if a > b then 3..14 2
1330 // 4 local t = { 4..7 3
1331 // 10 for i = 1, 10 do 10..13 3
1332 // 18 function M.second(c) 18..23 1
1333 // 19 while c > 0 do 19..21 2
1334 const NVIM_LUA_RANGES: &[(usize, usize)] =
1335 &[(2, 16), (3, 14), (4, 7), (10, 13), (18, 23), (19, 21)];
1336
1337 /// Closed auto folds, as `(start_row, end_row)`, after one pass at `fls`.
1338 fn closed_at(ranges: &[(usize, usize)], fls: u32) -> Vec<(usize, usize)> {
1339 let mut buf = View::from_str(&"x\n".repeat(26));
1340 buf.set_auto_folds(ranges, fls);
1341 buf.folds()
1342 .iter()
1343 .filter(|f| f.closed)
1344 .map(|f| (f.start_row, f.end_row))
1345 .collect()
1346 }
1347
1348 #[test]
1349 fn set_auto_folds_closes_folds_deeper_than_foldlevelstart() {
1350 // fls=0 → every fold closed (vim: level 1 > 0).
1351 assert_eq!(
1352 closed_at(NVIM_LUA_RANGES, 0),
1353 vec![(2, 16), (3, 14), (4, 7), (10, 13), (18, 23), (19, 21)],
1354 "foldlevelstart=0 must close every fold"
1355 );
1356 // fls=1 → level 1 open, levels 2+ closed. nvim closed 4..15 / 20..22
1357 // (1-based), i.e. the level-2 folds became the outermost closed ones.
1358 assert_eq!(
1359 closed_at(NVIM_LUA_RANGES, 1),
1360 vec![(3, 14), (4, 7), (10, 13), (19, 21)],
1361 "foldlevelstart=1 must open level 1 and close levels 2+"
1362 );
1363 // fls=2 → nvim closed 5..8 / 11..14 (1-based): the level-3 folds only.
1364 assert_eq!(
1365 closed_at(NVIM_LUA_RANGES, 2),
1366 vec![(4, 7), (10, 13)],
1367 "foldlevelstart=2 must close only level 3 and deeper"
1368 );
1369 // fls=3 and fls=99 → nvim closed nothing (max level here is 3).
1370 assert!(
1371 closed_at(NVIM_LUA_RANGES, 3).is_empty(),
1372 "foldlevelstart=3 must leave a 3-level nesting fully open"
1373 );
1374 assert!(
1375 closed_at(NVIM_LUA_RANGES, 99).is_empty(),
1376 "foldlevelstart=99 (hjkl's default) must open everything"
1377 );
1378 }
1379
1380 #[test]
1381 fn set_auto_folds_levels_are_independent_of_range_order() {
1382 // Tree-sitter query order is capture order, not document order: the
1383 // level sweep must sort, not trust the caller.
1384 let mut shuffled = NVIM_LUA_RANGES.to_vec();
1385 shuffled.reverse();
1386 assert_eq!(
1387 closed_at(&shuffled, 1),
1388 closed_at(NVIM_LUA_RANGES, 1),
1389 "nesting level must come from containment, not from range order"
1390 );
1391 }
1392
1393 #[test]
1394 fn set_auto_folds_levels_ignore_dropped_ranges() {
1395 // A single-row range never becomes a fold, so it must not push the
1396 // ranges around it a level deeper.
1397 let mut with_noise = NVIM_LUA_RANGES.to_vec();
1398 with_noise.push((5, 5)); // single row → dropped
1399 with_noise.push((9, 8)); // inverted → dropped
1400 assert_eq!(
1401 closed_at(&with_noise, 1),
1402 closed_at(NVIM_LUA_RANGES, 1),
1403 "ranges that never become folds must not count as a nesting level"
1404 );
1405 }
1406
1407 #[test]
1408 fn set_auto_folds_manual_folds_do_not_add_a_nesting_level() {
1409 // A `zf` fold wrapping the whole file must not make every auto fold
1410 // one level deeper — the auto set's levels are its own.
1411 let mut buf = View::from_str(&"x\n".repeat(26));
1412 buf.add_fold(0, 25, false);
1413 buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1414 let closed: Vec<(usize, usize)> = buf
1415 .folds()
1416 .iter()
1417 .filter(|f| f.auto_generated && f.closed)
1418 .map(|f| (f.start_row, f.end_row))
1419 .collect();
1420 assert_eq!(
1421 closed,
1422 vec![(3, 14), (4, 7), (10, 13), (19, 21)],
1423 "an enclosing manual fold must not shift auto fold levels"
1424 );
1425 }
1426
1427 #[test]
1428 fn set_auto_folds_at_a_level_settles_without_bumping_generations() {
1429 // The mixed open/closed set a non-zero `foldlevelstart` produces has
1430 // to compare equal on the next pass, or the caller's once-per-edit
1431 // guard fires every frame.
1432 let mut buf = View::from_str(&"x\n".repeat(26));
1433 buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1434 let fg = buf.fold_gen();
1435 let dg = buf.dirty_gen();
1436 for _ in 0..5 {
1437 buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1438 }
1439 assert_eq!(
1440 buf.fold_gen(),
1441 fg,
1442 "unchanged fold set must not bump fold_gen"
1443 );
1444 assert_eq!(
1445 buf.dirty_gen(),
1446 dg,
1447 "unchanged fold set must not bump dirty_gen"
1448 );
1449 }
1450
1451 // ── row-delta shifting (audit-r2 fix 1) ───────────────────────────────
1452
1453 fn fold(s: usize, e: usize) -> super::Fold {
1454 super::Fold {
1455 start_row: s,
1456 end_row: e,
1457 closed: true,
1458 auto_generated: false,
1459 }
1460 }
1461
1462 #[test]
1463 fn shift_fold_insert_above_shifts_down() {
1464 // 10-line file, fold at rows 4..6, insert one row at row 0
1465 // (`ggO x<Esc>`): vim shifts the fold to 5..7.
1466 let f = fold(4, 6);
1467 let shifted = super::shift_fold(f, 0, 0, 1, 1).unwrap();
1468 assert_eq!((shifted.start_row, shifted.end_row), (5, 7));
1469 }
1470
1471 #[test]
1472 fn shift_fold_delete_above_shifts_up() {
1473 // Fold at rows 4..6, one row deleted above at row 0.
1474 let f = fold(4, 6);
1475 let shifted = super::shift_fold(f, 0, 1, 1, -1).unwrap();
1476 assert_eq!((shifted.start_row, shifted.end_row), (3, 5));
1477 }
1478
1479 #[test]
1480 fn shift_fold_delete_fully_overlapping_drops() {
1481 // Fold at rows 4..6, deletion covers rows 4..6 entirely.
1482 let f = fold(4, 6);
1483 assert!(super::shift_fold(f, 4, 7, 7, -3).is_none());
1484 }
1485
1486 #[test]
1487 fn shift_fold_delete_overlapping_tail_clips() {
1488 // Fold at rows 4..6, deletion of rows 6..8 (tail only) clips the
1489 // fold to end at the last surviving row.
1490 let f = fold(4, 6);
1491 let shifted = super::shift_fold(f, 6, 9, 9, -3).unwrap();
1492 assert_eq!((shifted.start_row, shifted.end_row), (4, 5));
1493 }
1494
1495 #[test]
1496 fn shift_fold_delete_overlapping_head_clips() {
1497 // Fold at rows 4..6, deletion of rows 3..4 (head only) clips the
1498 // fold to start where the surviving tail now sits.
1499 let f = fold(4, 6);
1500 let shifted = super::shift_fold(f, 3, 5, 5, -2).unwrap();
1501 assert_eq!((shifted.start_row, shifted.end_row), (3, 4));
1502 }
1503
1504 #[test]
1505 fn shift_fold_edit_inside_grows_on_insert() {
1506 // Fold at rows 4..6, a line inserted at row 5 (strictly inside):
1507 // vim grows the fold's end, leaves the start alone.
1508 let f = fold(4, 6);
1509 let shifted = super::shift_fold(f, 5, 5, 6, 1).unwrap();
1510 assert_eq!((shifted.start_row, shifted.end_row), (4, 7));
1511 }
1512
1513 #[test]
1514 fn shift_fold_edit_inside_shrinks_on_delete() {
1515 // Fold at rows 4..8, rows 5..6 deleted (strictly inside): the fold
1516 // shrinks around the deletion instead of clipping or dropping.
1517 let f = fold(4, 8);
1518 let shifted = super::shift_fold(f, 5, 7, 7, -2).unwrap();
1519 assert_eq!((shifted.start_row, shifted.end_row), (4, 6));
1520 }
1521
1522 #[test]
1523 fn shift_fold_unaffected_when_entirely_before_edit() {
1524 let f = fold(1, 2);
1525 let shifted = super::shift_fold(f, 10, 11, 11, 1).unwrap();
1526 assert_eq!((shifted.start_row, shifted.end_row), (1, 2));
1527 }
1528
1529 #[test]
1530 fn shift_fold_zero_delta_is_noop() {
1531 let f = fold(4, 6);
1532 let shifted = super::shift_fold(f, 0, 0, 0, 0).unwrap();
1533 assert_eq!(shifted, f);
1534 }
1535
1536 #[test]
1537 fn shift_folds_after_edit_shifts_vec_in_place() {
1538 let mut folds = vec![fold(4, 6), fold(1, 2)];
1539 // Insert one row at row 0: both folds shift down.
1540 super::shift_folds_after_edit(&mut folds, 0, 0, 1, 1);
1541 let ranges: Vec<(usize, usize)> = folds.iter().map(|f| (f.start_row, f.end_row)).collect();
1542 assert_eq!(ranges, vec![(2, 3), (5, 7)]);
1543 }
1544
1545 #[test]
1546 fn shift_folds_after_edit_restores_canonical_order_after_start_collapse() {
1547 let mut folds = vec![fold(0, 3), fold(1, 5)];
1548
1549 super::shift_folds_after_edit(&mut folds, 0, 2, 2, -2);
1550
1551 assert_eq!(
1552 folds
1553 .iter()
1554 .map(|fold| (fold.start_row, fold.end_row))
1555 .collect::<Vec<_>>(),
1556 vec![(0, 3), (0, 1)]
1557 );
1558 let index = super::FoldIndex::new(&folds);
1559 assert!(index.hides_row(2));
1560 }
1561
1562 #[test]
1563 fn shift_folds_after_edit_deduplicates_exact_range_collisions() {
1564 let mut folds = vec![
1565 super::Fold {
1566 start_row: 0,
1567 end_row: 3,
1568 closed: false,
1569 auto_generated: false,
1570 },
1571 super::Fold {
1572 start_row: 1,
1573 end_row: 3,
1574 closed: true,
1575 auto_generated: true,
1576 },
1577 ];
1578
1579 super::shift_folds_after_edit(&mut folds, 0, 2, 2, -2);
1580
1581 assert_eq!(
1582 folds,
1583 vec![super::Fold {
1584 start_row: 0,
1585 end_row: 1,
1586 closed: false,
1587 auto_generated: false,
1588 }]
1589 );
1590 }
1591
1592 #[test]
1593 fn shift_folds_after_edit_drops_fully_consumed() {
1594 let mut folds = vec![fold(4, 6)];
1595 super::shift_folds_after_edit(&mut folds, 4, 7, 7, -3);
1596 assert!(folds.is_empty());
1597 }
1598
1599 // ── Borrow-style accessors + fold generation (round-2 perf item 10) ───
1600
1601 #[test]
1602 fn with_folds_sees_the_same_data_as_folds() {
1603 let mut buf = b();
1604 // Empty case.
1605 assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
1606 assert!(!buf.has_folds());
1607
1608 buf.add_fold(0, 1, false);
1609 buf.add_fold(2, 3, true);
1610 assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
1611 assert!(buf.has_folds());
1612 // And the borrow-style scan agrees with the owning one row by row.
1613 for row in 0..buf.row_count() {
1614 assert_eq!(
1615 buf.with_folds(|f| f.iter().any(|f| f.hides(row))),
1616 buf.folds().iter().any(|f| f.hides(row)),
1617 "row {row}"
1618 );
1619 }
1620 }
1621
1622 #[test]
1623 fn fold_gen_bumps_on_every_mutator() {
1624 fn none(_: &mut View) {}
1625 fn open_fold(b: &mut View) {
1626 b.add_fold(1, 3, false);
1627 }
1628 fn closed_fold(b: &mut View) {
1629 b.add_fold(1, 3, true);
1630 }
1631 /// (name, setup, mutation-that-must-bump)
1632 type Case = (&'static str, fn(&mut View), fn(&mut View));
1633 let cases: [Case; 13] = [
1634 ("add_fold", none, |b| b.add_fold(1, 3, false)),
1635 ("set_auto_folds", none, |b| b.set_auto_folds(&[(1, 3)], 99)),
1636 ("remove_fold_at", open_fold, |b| {
1637 assert!(b.remove_fold_at(2));
1638 }),
1639 ("open_fold_at", closed_fold, |b| {
1640 assert!(b.open_fold_at(2));
1641 }),
1642 ("close_fold_at", open_fold, |b| {
1643 assert!(b.close_fold_at(2));
1644 }),
1645 ("toggle_fold_at", open_fold, |b| {
1646 assert!(b.toggle_fold_at(2));
1647 }),
1648 ("open_all_folds", closed_fold, View::open_all_folds),
1649 ("close_all_folds", open_fold, View::close_all_folds),
1650 ("clear_all_folds", open_fold, View::clear_all_folds),
1651 ("reveal_row", closed_fold, |b| {
1652 assert!(b.reveal_row(2));
1653 }),
1654 ("invalidate_folds_in_range", closed_fold, |b| {
1655 b.invalidate_folds_in_range(2, 2);
1656 }),
1657 ("rebase_folds", closed_fold, |b| b.rebase_folds(0, 0, 1, 1)),
1658 ("set_folds", none, |b| {
1659 b.set_folds(&[super::Fold {
1660 start_row: 1,
1661 end_row: 3,
1662 closed: true,
1663 auto_generated: false,
1664 }]);
1665 }),
1666 ];
1667 for (name, setup, mutate) in cases {
1668 let mut buf = b();
1669 setup(&mut buf);
1670 let before = buf.fold_gen();
1671 mutate(&mut buf);
1672 assert!(
1673 buf.fold_gen() > before,
1674 "{name} mutated the fold set and must bump fold_gen"
1675 );
1676 }
1677 }
1678
1679 #[test]
1680 fn fold_gen_is_stable_across_reads_and_plain_text_edits() {
1681 let mut buf = b();
1682 buf.add_fold(1, 3, true);
1683 let fg = buf.fold_gen();
1684 assert!(fg > 0);
1685
1686 // Pure reads.
1687 let _ = buf.folds();
1688 let _ = buf.with_folds(<[super::Fold]>::to_vec);
1689 let _ = buf.has_folds();
1690 let _ = buf.is_row_hidden(2);
1691 let _ = buf.fold_at_row(2);
1692 let _ = buf.next_visible_row(1);
1693 let _ = buf.prev_visible_row(4);
1694 assert_eq!(buf.fold_gen(), fg, "read-only queries must not bump");
1695
1696 // No-op mutators (nothing actually changes).
1697 buf.close_all_folds(); // already closed
1698 buf.add_fold(9, 9, true); // out of bounds → rejected
1699 buf.rebase_folds(0, 0, 1, 0); // delta == 0 → early return
1700 let same = buf.folds();
1701 buf.set_folds(&same); // identical set
1702 assert_eq!(buf.fold_gen(), fg, "no-op mutators must not bump");
1703
1704 // A plain text edit must not masquerade as a fold change — that is
1705 // the whole reason `fold_gen` is separate from `dirty_gen`.
1706 let dg = buf.dirty_gen();
1707 buf.apply_edit(crate::Edit::InsertChar {
1708 at: crate::Position { row: 0, col: 0 },
1709 ch: 'x',
1710 });
1711 assert_ne!(buf.dirty_gen(), dg, "text edit must bump dirty_gen");
1712 assert_eq!(buf.fold_gen(), fg, "text edit must not bump fold_gen");
1713 }
1714
1715 #[test]
1716 fn rebase_folds_shifts_buffer_fold_storage() {
1717 let mut buf = View::from_str("0\n1\n2\n3\n4\n5\n6\n7\n8\n9");
1718 buf.add_fold(4, 6, true);
1719 buf.rebase_folds(0, 0, 1, 1);
1720 let folds = buf.folds();
1721 assert_eq!(folds.len(), 1);
1722 assert_eq!((folds[0].start_row, folds[0].end_row), (5, 7));
1723 }
1724}