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
66impl crate::View {
67 /// Returns a snapshot of all folds as an owned `Vec<Fold>`.
68 ///
69 /// Owned rather than `&[Fold]` because a `View` is a per-window
70 /// view onto a shared `Buffer`; another view could mutate the folds vec
71 /// between when this returns and when the caller reads the slice.
72 pub fn folds(&self) -> Vec<Fold> {
73 self.content_lock().folds.clone()
74 }
75
76 /// Run `f` against the fold list under a **single** content lock, with
77 /// no clone. The borrow-style twin of [`Self::folds`] — prefer it for
78 /// every read-only query (`hides` scans, `is_empty`, per-row loops);
79 /// keep [`Self::folds`] only where an owned snapshot must outlive the
80 /// lock (e.g. it is stored, or the buffer is re-borrowed mutably).
81 ///
82 /// The closure runs with the content mutex held: it must not call back
83 /// into any `&self` method of this `View` (they all re-lock, and the
84 /// mutex is not re-entrant). Hoist such reads — `row_count()`,
85 /// `cursor()`, … — above the call.
86 pub fn with_folds<T>(&self, f: impl FnOnce(&[Fold]) -> T) -> T {
87 f(&self.content_lock().folds)
88 }
89
90 /// True when at least one fold is defined (open or closed). One lock,
91 /// no clone — the cheap form of `!folds().is_empty()`.
92 pub fn has_folds(&self) -> bool {
93 !self.content_lock().folds.is_empty()
94 }
95
96 /// Monotonic fold-mutation generation. Bumps only when a fold mutator
97 /// actually changes the fold set; read-only queries and plain text
98 /// edits leave it alone. Hosts caching a fold snapshot (`hjkl`'s
99 /// per-window `window_folds`) compare this instead of re-cloning the
100 /// fold `Vec` every keystroke.
101 ///
102 /// Conservative in the same sense as [`Self::dirty_gen`]: "if it
103 /// changed, the folds **may** have changed" (a `rebase_folds` whose
104 /// row-shift happens to move nothing still bumps).
105 pub fn fold_gen(&self) -> u64 {
106 self.content_lock().fold_gen
107 }
108
109 /// Record a fold-set mutation: bump the fold generation *and* the
110 /// render-cache generation (a fold change repaints). Every mutator in
111 /// this module funnels through here.
112 fn folds_changed(&mut self) {
113 self.fold_gen_bump();
114 self.dirty_gen_bump();
115 }
116
117 /// Register a new fold. If an existing fold has the same
118 /// `start_row`, it's replaced; otherwise the new one is inserted
119 /// in start-row order. Empty / inverted ranges are rejected.
120 pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
121 if end_row < start_row {
122 return;
123 }
124 let last = self.row_count().saturating_sub(1);
125 if start_row > last {
126 return;
127 }
128 let end_row = end_row.min(last);
129 let fold = Fold {
130 start_row,
131 end_row,
132 closed,
133 auto_generated: false,
134 };
135 {
136 let mut c = self.content_lock_mut();
137 if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
138 c.folds[idx] = fold;
139 } else {
140 let pos = c
141 .folds
142 .iter()
143 .position(|f| f.start_row > start_row)
144 .unwrap_or(c.folds.len());
145 c.folds.insert(pos, fold);
146 }
147 }
148 self.folds_changed();
149 }
150
151 /// Replace all auto-generated folds with a new set derived from
152 /// `ranges`, while leaving manual folds untouched.
153 ///
154 /// ## `foldlevelstart`, not a boolean
155 ///
156 /// The second parameter is vim's `'foldlevelstart'` verbatim, and the rule
157 /// it implements is vim's: **a fold whose (1-based) nesting level is
158 /// greater than `foldlevelstart` starts closed**, everything at or above
159 /// that level starts open. So `0` closes everything, `1` leaves top-level
160 /// folds open and closes what is inside them, `99` (hjkl's default) opens
161 /// everything.
162 ///
163 /// It takes the raw option rather than a `default_closed: bool` — the
164 /// shape it replaced, which could only express "all closed" / "all open" —
165 /// and rather than a per-range level supplied by the caller, because:
166 ///
167 /// - **Nesting level is derivable from the ranges alone** (a fold's level
168 /// is one more than the number of other ranges that strictly contain
169 /// it), so a caller passing levels would be passing information this
170 /// function can compute — and would have to recompute identically at
171 /// every call site, marker scan and tree-sitter query alike.
172 /// - **Only this function knows the final set.** Levels have to be counted
173 /// over the ranges that actually become folds, and that is decided
174 /// *here*: single-row, inverted and out-of-range entries are dropped,
175 /// `end_row` is clamped, duplicate start rows collapse, and rows owned
176 /// by a manual fold are skipped. A level computed by the caller over the
177 /// raw ranges would be counting folds that do not exist.
178 ///
179 /// Levels are counted over the **auto** folds only: a `zf` fold enclosing
180 /// an auto one is the user's own structure, and letting it push the auto
181 /// fold a level deeper would make the auto fold's start state depend on
182 /// unrelated manual folding.
183 ///
184 /// vim's own default for the option is `-1`, meaning "do nothing, leave
185 /// `'foldlevel'` alone" (which, with `'foldlevel'` at its own default of
186 /// `0`, ends up closing everything). hjkl's `foldlevelstart` is a `u32`
187 /// defaulting to `99`, so that mode does not exist here and no value is
188 /// reserved for it — every value is a real level.
189 ///
190 /// ## Algorithm (O(N log N) — bounded by `ranges.len()`, no unbounded growth)
191 ///
192 /// 1. Snapshot `start_row → closed` for every existing auto fold so
193 /// open/closed state survives a reparse.
194 /// 2. Retain only manual folds (`auto_generated == false`).
195 /// 3. Normalise `ranges` into the set that will actually become folds.
196 /// 4. Assign each of those a nesting level by a containment sweep.
197 /// 5. Insert one new `Fold` per surviving range, re-using the snapshotted
198 /// closed state when the start_row existed before, else
199 /// `level > foldlevelstart`.
200 ///
201 /// Invariants preserved:
202 /// - Folds stay sorted by `start_row` (same ordering as `add_fold`).
203 /// - Duplicate start_rows: the last range in `ranges` wins (consistent
204 /// with `add_fold`'s replace-on-same-start-row semantics). In practice
205 /// TS query ranges are already deduplicated.
206 /// - Empty / inverted ranges (end_row < start_row) are silently skipped.
207 /// - `end_row` is clamped to the last valid row, same as `add_fold`.
208 /// - A MANUAL fold's start_row is never taken over: the auto range for
209 /// that row is dropped instead. `zf` is an explicit choice of extent,
210 /// and converting it to an auto fold both changed it and handed it to
211 /// the auto engine to overwrite on the next reparse.
212 /// - An auto fold at a start_row that already had one keeps its open /
213 /// closed state: `foldlevelstart` decides how a fold *starts*, so it
214 /// applies to newly-appearing rows only and never re-closes a fold the
215 /// user opened.
216 /// - When the resulting fold set is identical to the current one, nothing
217 /// is written and NO generation bumps — [`Self::fold_gen`] promises to
218 /// move only on a real change, and `dirty_gen` moving every pass made
219 /// the caller's "recompute once per edit" guard fire every frame (and
220 /// with it a full re-highlight, since `dirty_gen` keys that cache).
221 pub fn set_auto_folds(&mut self, ranges: &[(usize, usize)], foldlevelstart: u32) {
222 // 1. Snapshot closed state of existing auto folds by start_row.
223 let prev_closed: std::collections::HashMap<usize, bool> = self
224 .content_lock()
225 .folds
226 .iter()
227 .filter(|f| f.auto_generated)
228 .map(|f| (f.start_row, f.closed))
229 .collect();
230
231 // 2. Start from the manual folds — they survive untouched, and their
232 // start rows are off-limits to the ranges below.
233 let mut next: Vec<Fold> = self
234 .content_lock()
235 .folds
236 .iter()
237 .filter(|f| !f.auto_generated)
238 .copied()
239 .collect();
240 let manual_starts: std::collections::HashSet<usize> =
241 next.iter().map(|f| f.start_row).collect();
242
243 // 3. Normalise: drop what will never become a fold, so the level
244 // sweep below counts only real folds. Start rows end up unique.
245 let last = self.row_count().saturating_sub(1);
246 let mut accepted: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
247 for &(start_row, end_row) in ranges {
248 // Skip empty/inverted and out-of-bounds ranges.
249 if end_row < start_row || start_row > last {
250 continue;
251 }
252 let end_row = end_row.min(last);
253 // Only folds spanning more than one row are meaningful.
254 if end_row == start_row {
255 continue;
256 }
257 // A manual fold owns this row — leave it alone.
258 if manual_starts.contains(&start_row) {
259 continue;
260 }
261 match accepted.iter().position(|r| r.0 == start_row) {
262 Some(idx) => accepted[idx] = (start_row, end_row),
263 None => accepted.push((start_row, end_row)),
264 }
265 }
266
267 // 4. Nesting level per accepted range, by a single containment sweep.
268 // Visiting outermost-first (start ascending, end descending) makes
269 // `stack` the chain of enclosing folds: pop everything that ends
270 // before this range does — that covers both a sibling that already
271 // closed and a partial overlap, neither of which encloses it — and
272 // what is left is exactly the enclosing chain.
273 let levels = {
274 let mut order: Vec<usize> = (0..accepted.len()).collect();
275 order.sort_by_key(|&i| (accepted[i].0, std::cmp::Reverse(accepted[i].1)));
276 let mut levels = vec![0u32; accepted.len()];
277 let mut stack: Vec<usize> = Vec::new();
278 for i in order {
279 let end_row = accepted[i].1;
280 while stack.last().is_some_and(|&open_end| open_end < end_row) {
281 stack.pop();
282 }
283 // 1-based, matching vim: an unnested fold is level 1.
284 levels[i] = stack.len() as u32 + 1;
285 stack.push(end_row);
286 }
287 levels
288 };
289
290 // 5. Insert new auto folds in sorted order.
291 for (i, &(start_row, end_row)) in accepted.iter().enumerate() {
292 let closed = prev_closed
293 .get(&start_row)
294 .copied()
295 .unwrap_or(levels[i] > foldlevelstart);
296 let fold = Fold {
297 start_row,
298 end_row,
299 closed,
300 auto_generated: true,
301 };
302 match next.iter().position(|f| f.start_row == start_row) {
303 Some(idx) => next[idx] = fold,
304 None => {
305 let pos = next
306 .iter()
307 .position(|f| f.start_row > start_row)
308 .unwrap_or(next.len());
309 next.insert(pos, fold);
310 }
311 }
312 }
313
314 // 6. No-op when nothing actually changed — see the invariant above.
315 if self.content_lock().folds == next {
316 return;
317 }
318 self.content_lock_mut().folds = next;
319 self.folds_changed();
320 }
321
322 /// Drop the fold whose range covers `row`. Returns `true` when a
323 /// fold was actually removed.
324 pub fn remove_fold_at(&mut self, row: usize) -> bool {
325 // Remove the INNERMOST fold containing `row` (largest start_row), so
326 // `zd` on a nested fold drops the inner one, not the enclosing block.
327 let idx = self
328 .content_lock()
329 .folds
330 .iter()
331 .enumerate()
332 .filter(|(_, f)| f.contains(row))
333 .max_by_key(|(_, f)| f.start_row)
334 .map(|(i, _)| i);
335 let Some(idx) = idx else {
336 return false;
337 };
338 self.content_lock_mut().folds.remove(idx);
339 self.folds_changed();
340 true
341 }
342
343 /// Open the fold at `row` (no-op if already open or no fold).
344 pub fn open_fold_at(&mut self, row: usize) -> bool {
345 let changed = {
346 let mut c = self.content_lock_mut();
347 let Some(f) = c
348 .folds
349 .iter_mut()
350 .filter(|f| f.contains(row))
351 .max_by_key(|f| f.start_row)
352 else {
353 return false;
354 };
355 if !f.closed {
356 return false;
357 }
358 f.closed = false;
359 true
360 };
361 if changed {
362 self.folds_changed();
363 }
364 changed
365 }
366
367 /// Close the fold at `row` (no-op if already closed or no fold).
368 pub fn close_fold_at(&mut self, row: usize) -> bool {
369 let changed = {
370 let mut c = self.content_lock_mut();
371 let Some(f) = c
372 .folds
373 .iter_mut()
374 .filter(|f| f.contains(row))
375 .max_by_key(|f| f.start_row)
376 else {
377 return false;
378 };
379 if f.closed {
380 return false;
381 }
382 f.closed = true;
383 true
384 };
385 if changed {
386 self.folds_changed();
387 }
388 changed
389 }
390
391 /// Flip the closed/open state of the fold containing `row`.
392 pub fn toggle_fold_at(&mut self, row: usize) -> bool {
393 let changed = {
394 let mut c = self.content_lock_mut();
395 let Some(f) = c
396 .folds
397 .iter_mut()
398 .filter(|f| f.contains(row))
399 .max_by_key(|f| f.start_row)
400 else {
401 return false;
402 };
403 f.closed = !f.closed;
404 true
405 };
406 if changed {
407 self.folds_changed();
408 }
409 changed
410 }
411
412 /// `zR` — open every fold.
413 pub fn open_all_folds(&mut self) {
414 let changed = {
415 let mut c = self.content_lock_mut();
416 let mut any = false;
417 for f in c.folds.iter_mut() {
418 if f.closed {
419 f.closed = false;
420 any = true;
421 }
422 }
423 any
424 };
425 if changed {
426 self.folds_changed();
427 }
428 }
429
430 /// `zE` — eliminate every fold.
431 pub fn clear_all_folds(&mut self) {
432 let was_nonempty = !self.content_lock().folds.is_empty();
433 if was_nonempty {
434 self.content_lock_mut().folds.clear();
435 self.folds_changed();
436 }
437 }
438
439 /// `zM` — close every fold.
440 pub fn close_all_folds(&mut self) {
441 let changed = {
442 let mut c = self.content_lock_mut();
443 let mut any = false;
444 for f in c.folds.iter_mut() {
445 if !f.closed {
446 f.closed = true;
447 any = true;
448 }
449 }
450 any
451 };
452 if changed {
453 self.folds_changed();
454 }
455 }
456
457 /// First fold whose range contains `row`. Useful for the host's
458 /// `za`/`zo`/`zc` handlers.
459 pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
460 // Innermost fold containing `row`: with nested folds, the one with the
461 // largest `start_row` is the most-deeply-nested. Folds are stored in
462 // start-row order, so a plain `.find` would return the OUTERMOST fold
463 // and `zc`/`za`/`zo` would act on the wrong level.
464 self.content_lock()
465 .folds
466 .iter()
467 .filter(|f| f.contains(row))
468 .max_by_key(|f| f.start_row)
469 .copied()
470 }
471
472 /// True iff `row` is hidden by a closed fold (any fold).
473 pub fn is_row_hidden(&self, row: usize) -> bool {
474 self.with_folds(|folds| folds.iter().any(|f| f.hides(row)))
475 }
476
477 /// Open every closed fold whose body hides `row`, so the row becomes
478 /// visible. Handles nested folds in a single pass — unlike
479 /// `open_fold_at` / `FoldOp::OpenAt`, which only act on the first fold
480 /// containing the row and so can never reach a nested inner fold.
481 /// Used by `goto_line` so a jump into a folded region reveals the
482 /// target line instead of stranding the cursor on a hidden row.
483 /// Returns `true` if any fold was opened.
484 pub fn reveal_row(&mut self, row: usize) -> bool {
485 let changed = {
486 let mut c = self.content_lock_mut();
487 let mut any = false;
488 for f in c.folds.iter_mut() {
489 if f.hides(row) {
490 f.closed = false;
491 any = true;
492 }
493 }
494 any
495 };
496 if changed {
497 self.folds_changed();
498 }
499 changed
500 }
501
502 /// Last row index containing real content — skips vim's single
503 /// phantom trailing empty row. `ropey`'s `len_lines()` always
504 /// synthesizes one extra empty final "line" when the buffer text
505 /// ends in `\n` (vim treats that `\n` as a terminator, not a
506 /// separator). Mirrors `hjkl_engine::motions::move_bottom`'s clamp
507 /// (`G`) so vertical motions agree with `G` on where the buffer
508 /// "ends". A buffer whose *real* last line happens to be empty
509 /// (e.g. `"foo\n\n"`, row 1) is untouched — only a single trailing
510 /// phantom row is ever skipped.
511 ///
512 /// The emptiness test reads the last row's byte length rather than
513 /// materializing the row as a `String`: for the final rope line the two
514 /// agree exactly (ropey never gives the last line a trailing `\n`, so
515 /// `rope_line_str` has nothing to strip and
516 /// `rope_line_bytes(last) == 0` iff `rope_line_str(last).is_empty()`).
517 pub fn last_content_row(&self) -> usize {
518 let raw_last = self.row_count().saturating_sub(1);
519 if raw_last > 0 {
520 let c = self.content_lock();
521 if crate::buffer::rope_line_bytes(&c.text, raw_last) == 0 {
522 return raw_last - 1;
523 }
524 }
525 raw_last
526 }
527
528 /// First visible row strictly after `row`, skipping any rows hidden
529 /// by closed folds. Returns `None` past the end of the buffer.
530 ///
531 /// Takes the content lock **once** for the whole walk (via
532 /// [`Self::with_folds`]) instead of once per skipped row — `j` over a
533 /// long closed fold used to pay a lock + full `Vec<Fold>` clone per row.
534 /// `last_content_row()` locks too, so it is resolved before the scan.
535 pub fn next_visible_row(&self, row: usize) -> Option<usize> {
536 let last = self.last_content_row();
537 if last == 0 && row == 0 {
538 return None;
539 }
540 let mut r = row.checked_add(1)?;
541 self.with_folds(|folds| {
542 while r <= last && folds.iter().any(|f| f.hides(r)) {
543 r += 1;
544 }
545 (r <= last).then_some(r)
546 })
547 }
548
549 /// First visible row strictly before `row`, skipping hidden rows.
550 ///
551 /// One lock for the whole walk, same as [`Self::next_visible_row`].
552 pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
553 let mut r = row.checked_sub(1)?;
554 self.with_folds(|folds| {
555 while folds.iter().any(|f| f.hides(r)) {
556 r = r.checked_sub(1)?;
557 }
558 Some(r)
559 })
560 }
561
562 /// Drop every fold that touches `[start_row, end_row]`.
563 pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
564 let before = self.content_lock().folds.len();
565 invalidate_folds(&mut self.content_lock_mut().folds, start_row, end_row);
566 if self.content_lock().folds.len() != before {
567 self.folds_changed();
568 }
569 }
570
571 /// Shift every buffer fold by an edit's row-delta band. Mirrors
572 /// [`crate::buffer::View::rebase_marks`] for the shared fold storage —
573 /// see [`shift_folds_after_edit`] for the per-fold rules.
574 pub fn rebase_folds(
575 &mut self,
576 edit_start: usize,
577 drop_end: usize,
578 shift_threshold: usize,
579 delta: isize,
580 ) {
581 if delta == 0 {
582 return;
583 }
584 let touched = {
585 let mut c = self.content_lock_mut();
586 if c.folds.is_empty() {
587 false
588 } else {
589 shift_folds_after_edit(&mut c.folds, edit_start, drop_end, shift_threshold, delta);
590 true
591 }
592 };
593 if touched {
594 // Conservative: a shift that happened to move nothing (every
595 // fold entirely below the edit) still bumps. Matches the
596 // `dirty_gen` contract — "may have changed".
597 self.fold_gen_bump();
598 }
599 }
600
601 /// Replace the entire fold set wholesale. Used to install a per-window fold
602 /// snapshot into the shared buffer on focus change (window-level folds): the
603 /// app keeps each window's open/closed state and swaps it in before dispatch,
604 /// so motions/render/`z`-ops operate on the focused window's folds.
605 pub fn set_folds(&mut self, folds: &[Fold]) {
606 {
607 let mut c = self.content_lock_mut();
608 if c.folds.as_slice() == folds {
609 return; // no-op — avoid a spurious dirty_gen bump
610 }
611 c.folds = folds.to_vec();
612 }
613 self.folds_changed();
614 }
615}
616
617/// Drop every fold in `folds` that touches `[start_row, end_row]`, in place.
618///
619/// Free helper so both [`crate::View::invalidate_folds_in_range`] (operating
620/// on the shared content) and the app's window-level edit-coherence pass
621/// (operating on a sibling window's owned `Vec<Fold>`) share one rule — vim
622/// opens/forgets any fold the edit overlapped.
623pub fn invalidate_folds(folds: &mut Vec<Fold>, start_row: usize, end_row: usize) {
624 folds.retain(|f| f.end_row < start_row || f.start_row > end_row);
625}
626
627// ── Row-delta shifting (edit-coherence) ──────────────────────────────────
628//
629// A manual (`zf`) fold is a row-range that has to track the same
630// insert/delete row-shift the engine already applies to marks and the
631// jumplist (see `Editor::shift_marks_after_edit`). Without this, a fold
632// below an edit keeps stale row numbers and the renderer / fold-aware ops
633// (`dd`, `p`, …) act on the wrong rows (#audit-r2 fix 1).
634//
635// The four `(edit_start, drop_end, shift_threshold, delta)` parameters are
636// the exact same band description `Editor::shift_marks_after_edit` computes
637// for marks: `[edit_start, drop_end)` is the row band the edit deleted
638// (empty for inserts), and any row `>= shift_threshold` moves by `delta`.
639// Reusing the identical band keeps folds, marks, and jumplist entries
640// shifting in lockstep for the same edit.
641
642/// Shift a single fold's `start_row` / `end_row` by an edit's row-delta band.
643/// Returns `None` when the edit's deleted band fully consumes the fold.
644///
645/// Each endpoint is mapped independently through the same drop/shift rule
646/// [`crate::buffer::View::rebase_marks`] applies to a point mark. Mapping
647/// the two endpoints independently is what produces the vim-shaped "edit
648/// inside a fold" semantics for free:
649/// - Both endpoints below `shift_threshold` and outside the deleted band →
650/// fold untouched (edit happened entirely outside the fold).
651/// - `start_row` outside the deleted band but `end_row` inside it → the
652/// edit deleted the fold's tail; it clips to end at the last surviving
653/// row (`edit_start - 1`).
654/// - `start_row` inside the deleted band but `end_row` outside it → the
655/// edit deleted the fold's head; it clips to start at `edit_start` (the
656/// row the surviving tail now occupies).
657/// - Both endpoints inside the deleted band → the edit consumed the whole
658/// fold; it's dropped.
659/// - `start_row` below the threshold and `end_row` at/above it (an insert
660/// or a deletion landing strictly inside the fold) → `start_row` stays,
661/// `end_row` shifts by `delta`: the fold grows (insert) or shrinks
662/// (delete) around the edit, matching vim.
663/// - Both endpoints at/above the threshold → the fold shifts wholesale.
664pub fn shift_fold(
665 fold: Fold,
666 edit_start: usize,
667 drop_end: usize,
668 shift_threshold: usize,
669 delta: isize,
670) -> Option<Fold> {
671 if delta == 0 {
672 return Some(fold);
673 }
674 let map_row = |row: usize| -> Option<usize> {
675 if (edit_start..drop_end).contains(&row) {
676 None
677 } else if row >= shift_threshold {
678 Some(((row as isize) + delta).max(0) as usize)
679 } else {
680 Some(row)
681 }
682 };
683 let mapped_start = map_row(fold.start_row);
684 let mapped_end = map_row(fold.end_row);
685 if mapped_start.is_none() && mapped_end.is_none() {
686 return None;
687 }
688 let new_start = mapped_start.unwrap_or(edit_start);
689 let new_end = mapped_end.unwrap_or_else(|| edit_start.saturating_sub(1));
690 if new_end < new_start {
691 return None;
692 }
693 Some(Fold {
694 start_row: new_start,
695 end_row: new_end,
696 closed: fold.closed,
697 auto_generated: fold.auto_generated,
698 })
699}
700
701/// Shift every fold in `folds` by an edit's row-delta band, in place.
702/// Folds the edit's deleted band fully consumes are dropped (mirrors
703/// [`invalidate_folds`] for the folds that DO survive but move).
704///
705/// Shared by [`crate::View::rebase_folds`] (engine-side, the buffer's own
706/// fold storage) and the app's sibling-window fold snapshot shift, so both
707/// converge on the identical row-shift rule.
708pub fn shift_folds_after_edit(
709 folds: &mut Vec<Fold>,
710 edit_start: usize,
711 drop_end: usize,
712 shift_threshold: usize,
713 delta: isize,
714) {
715 if delta == 0 {
716 return;
717 }
718 folds.retain_mut(
719 |f| match shift_fold(*f, edit_start, drop_end, shift_threshold, delta) {
720 Some(shifted) => {
721 *f = shifted;
722 true
723 }
724 None => false,
725 },
726 );
727}
728
729#[cfg(test)]
730mod tests {
731 use crate::View;
732
733 fn b() -> View {
734 View::from_str("a\nb\nc\nd\ne")
735 }
736
737 #[test]
738 fn add_keeps_folds_in_start_row_order() {
739 let mut buf = b();
740 buf.add_fold(2, 3, true);
741 buf.add_fold(0, 1, false);
742 let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
743 assert_eq!(starts, vec![0, 2]);
744 }
745
746 #[test]
747 fn set_folds_replaces_wholesale() {
748 let mut buf = b();
749 buf.add_fold(0, 1, false);
750 // Install a different per-window snapshot.
751 let snapshot = vec![super::Fold {
752 start_row: 2,
753 end_row: 3,
754 closed: true,
755 auto_generated: false,
756 }];
757 buf.set_folds(&snapshot);
758 assert_eq!(buf.folds(), snapshot);
759 // Idempotent: re-installing the same set is a no-op (no dirty bump).
760 let dg = buf.dirty_gen();
761 buf.set_folds(&snapshot);
762 assert_eq!(buf.dirty_gen(), dg);
763 }
764
765 #[test]
766 fn invalidate_folds_helper_drops_overlapping() {
767 let f = |s, e| super::Fold {
768 start_row: s,
769 end_row: e,
770 closed: true,
771 auto_generated: false,
772 };
773 let mut folds = vec![f(0, 2), f(4, 6), f(8, 10)];
774 // Edit touches rows 5..5 → only the [4,6] fold overlaps.
775 super::invalidate_folds(&mut folds, 5, 5);
776 let starts: Vec<usize> = folds.iter().map(|x| x.start_row).collect();
777 assert_eq!(starts, vec![0, 8]);
778 }
779
780 #[test]
781 fn add_replaces_existing_with_same_start_row() {
782 let mut buf = b();
783 buf.add_fold(1, 2, true);
784 buf.add_fold(1, 4, false);
785 assert_eq!(buf.folds().len(), 1);
786 assert_eq!(buf.folds()[0].end_row, 4);
787 assert!(!buf.folds()[0].closed);
788 }
789
790 #[test]
791 fn add_clamps_end_row_to_buffer_bounds() {
792 let mut buf = b();
793 buf.add_fold(2, 99, true);
794 assert_eq!(buf.folds()[0].end_row, 4);
795 }
796
797 #[test]
798 fn add_rejects_inverted_range() {
799 let mut buf = b();
800 buf.add_fold(3, 1, true);
801 assert!(buf.folds().is_empty());
802 }
803
804 #[test]
805 fn toggle_flips_state() {
806 let mut buf = b();
807 buf.add_fold(1, 3, false);
808 assert!(!buf.folds()[0].closed);
809 assert!(buf.toggle_fold_at(2));
810 assert!(buf.folds()[0].closed);
811 assert!(buf.toggle_fold_at(2));
812 assert!(!buf.folds()[0].closed);
813 }
814
815 #[test]
816 fn is_row_hidden_excludes_start_row() {
817 let mut buf = b();
818 buf.add_fold(1, 3, true);
819 assert!(!buf.is_row_hidden(0));
820 assert!(!buf.is_row_hidden(1)); // start row stays visible
821 assert!(buf.is_row_hidden(2));
822 assert!(buf.is_row_hidden(3));
823 assert!(!buf.is_row_hidden(4));
824 }
825
826 #[test]
827 fn open_close_all_changes_every_fold() {
828 let mut buf = b();
829 buf.add_fold(0, 1, false);
830 buf.add_fold(2, 3, true);
831 buf.close_all_folds();
832 assert!(buf.folds().iter().all(|f| f.closed));
833 buf.open_all_folds();
834 assert!(buf.folds().iter().all(|f| !f.closed));
835 }
836
837 #[test]
838 fn invalidate_drops_overlapping_folds() {
839 let mut buf = b();
840 buf.add_fold(0, 1, true);
841 buf.add_fold(2, 3, true);
842 buf.add_fold(4, 4, true);
843 buf.invalidate_folds_in_range(2, 3);
844 let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
845 assert_eq!(starts, vec![0, 4]);
846 }
847
848 // ── auto_generated flag + set_auto_folds ─────────────────────────────────
849
850 #[test]
851 fn add_fold_sets_auto_generated_false() {
852 let mut buf = b();
853 buf.add_fold(1, 3, false);
854 assert!(
855 !buf.folds()[0].auto_generated,
856 "manual add_fold must have auto_generated=false"
857 );
858 }
859
860 #[test]
861 fn set_auto_folds_adds_auto_folds() {
862 let mut buf = b();
863 buf.set_auto_folds(&[(0, 2), (3, 4)], 99);
864 let folds = buf.folds();
865 assert_eq!(folds.len(), 2);
866 assert!(folds[0].auto_generated);
867 assert!(folds[1].auto_generated);
868 assert_eq!(folds[0].start_row, 0);
869 assert_eq!(folds[1].start_row, 3);
870 }
871
872 #[test]
873 fn set_auto_folds_second_call_replaces_first() {
874 let mut buf = b();
875 buf.set_auto_folds(&[(0, 2), (3, 4)], 99);
876 assert_eq!(buf.folds().len(), 2);
877 // Replace with a different set.
878 buf.set_auto_folds(&[(1, 4)], 99);
879 let folds = buf.folds();
880 assert_eq!(folds.len(), 1, "second call must replace first set");
881 assert_eq!(folds[0].start_row, 1);
882 assert!(folds[0].auto_generated);
883 }
884
885 #[test]
886 fn set_auto_folds_preserves_manual_folds() {
887 let mut buf = b();
888 // Add a manual fold.
889 buf.add_fold(0, 1, true);
890 // Auto-fold the remaining range.
891 buf.set_auto_folds(&[(2, 4)], 99);
892 let folds = buf.folds();
893 assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
894 let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
895 assert!(!manual.auto_generated, "manual fold flag must stay false");
896 let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
897 assert!(auto.auto_generated);
898 }
899
900 #[test]
901 fn set_auto_folds_preserves_open_closed_state_by_start_row() {
902 let mut buf = b();
903 // First auto-fold pass: create a closed fold at row 0.
904 buf.set_auto_folds(&[(0, 2)], 0); // foldlevelstart=0 → starts closed
905 assert!(buf.folds()[0].closed, "fold must start closed per default");
906
907 // User opens the fold (simulated by toggle).
908 buf.toggle_fold_at(0);
909 assert!(!buf.folds()[0].closed, "fold must now be open");
910
911 // Second auto-fold pass with same start_row — must preserve open state.
912 buf.set_auto_folds(&[(0, 2)], 0); // foldlevelstart=0 but prev was open
913 assert!(
914 !buf.folds()[0].closed,
915 "open/closed state must be preserved across set_auto_folds"
916 );
917 }
918
919 #[test]
920 fn set_auto_folds_skips_single_row_and_inverted_ranges() {
921 let mut buf = b();
922 buf.set_auto_folds(&[(1, 1), (3, 2)], 99);
923 assert!(
924 buf.folds().is_empty(),
925 "single-row and inverted ranges must be skipped"
926 );
927 }
928
929 #[test]
930 fn set_auto_folds_new_folds_start_per_foldlevelstart() {
931 let mut buf = b();
932 buf.set_auto_folds(&[(0, 4)], 0);
933 assert!(
934 buf.folds()[0].closed,
935 "new auto fold must start closed at foldlevelstart=0"
936 );
937
938 // Re-running the same start_row at foldlevelstart=99 must NOT reopen
939 // it: the snapshot preserves the state the fold already has, and
940 // `foldlevelstart` only decides how a fold *starts*.
941 buf.set_auto_folds(&[(0, 4)], 99);
942 assert!(
943 buf.folds()[0].closed,
944 "an existing fold keeps its state — foldlevelstart is start-only"
945 );
946
947 // A brand-new start row takes the option: level 1 <= 99 → open.
948 let mut buf2 = b();
949 buf2.set_auto_folds(&[(2, 4)], 99);
950 assert!(
951 !buf2.folds()[0].closed,
952 "brand-new level-1 auto fold must start open at foldlevelstart=99"
953 );
954 }
955
956 // ── foldlevelstart: level semantics, not a boolean ────────────────────
957 //
958 // The expectations below are neovim 0.12.4's, measured rather than
959 // reasoned: the same nesting opened with `foldmethod=expr` +
960 // `v:lua.vim.treesitter.foldexpr()` and `foldlevelstart` set to each
961 // value, with the closed folds enumerated via `foldclosed` /
962 // `foldclosedend` (`foldlevel()` merges adjacent siblings and reads as a
963 // false difference). Converted from vim's 1-based lines to 0-based rows.
964 //
965 // row source fold level
966 // 2 function M.outer(a, b) 2..16 1
967 // 3 if a > b then 3..14 2
968 // 4 local t = { 4..7 3
969 // 10 for i = 1, 10 do 10..13 3
970 // 18 function M.second(c) 18..23 1
971 // 19 while c > 0 do 19..21 2
972 const NVIM_LUA_RANGES: &[(usize, usize)] =
973 &[(2, 16), (3, 14), (4, 7), (10, 13), (18, 23), (19, 21)];
974
975 /// Closed auto folds, as `(start_row, end_row)`, after one pass at `fls`.
976 fn closed_at(ranges: &[(usize, usize)], fls: u32) -> Vec<(usize, usize)> {
977 let mut buf = View::from_str(&"x\n".repeat(26));
978 buf.set_auto_folds(ranges, fls);
979 buf.folds()
980 .iter()
981 .filter(|f| f.closed)
982 .map(|f| (f.start_row, f.end_row))
983 .collect()
984 }
985
986 #[test]
987 fn set_auto_folds_closes_folds_deeper_than_foldlevelstart() {
988 // fls=0 → every fold closed (vim: level 1 > 0).
989 assert_eq!(
990 closed_at(NVIM_LUA_RANGES, 0),
991 vec![(2, 16), (3, 14), (4, 7), (10, 13), (18, 23), (19, 21)],
992 "foldlevelstart=0 must close every fold"
993 );
994 // fls=1 → level 1 open, levels 2+ closed. nvim closed 4..15 / 20..22
995 // (1-based), i.e. the level-2 folds became the outermost closed ones.
996 assert_eq!(
997 closed_at(NVIM_LUA_RANGES, 1),
998 vec![(3, 14), (4, 7), (10, 13), (19, 21)],
999 "foldlevelstart=1 must open level 1 and close levels 2+"
1000 );
1001 // fls=2 → nvim closed 5..8 / 11..14 (1-based): the level-3 folds only.
1002 assert_eq!(
1003 closed_at(NVIM_LUA_RANGES, 2),
1004 vec![(4, 7), (10, 13)],
1005 "foldlevelstart=2 must close only level 3 and deeper"
1006 );
1007 // fls=3 and fls=99 → nvim closed nothing (max level here is 3).
1008 assert!(
1009 closed_at(NVIM_LUA_RANGES, 3).is_empty(),
1010 "foldlevelstart=3 must leave a 3-level nesting fully open"
1011 );
1012 assert!(
1013 closed_at(NVIM_LUA_RANGES, 99).is_empty(),
1014 "foldlevelstart=99 (hjkl's default) must open everything"
1015 );
1016 }
1017
1018 #[test]
1019 fn set_auto_folds_levels_are_independent_of_range_order() {
1020 // Tree-sitter query order is capture order, not document order: the
1021 // level sweep must sort, not trust the caller.
1022 let mut shuffled = NVIM_LUA_RANGES.to_vec();
1023 shuffled.reverse();
1024 assert_eq!(
1025 closed_at(&shuffled, 1),
1026 closed_at(NVIM_LUA_RANGES, 1),
1027 "nesting level must come from containment, not from range order"
1028 );
1029 }
1030
1031 #[test]
1032 fn set_auto_folds_levels_ignore_dropped_ranges() {
1033 // A single-row range never becomes a fold, so it must not push the
1034 // ranges around it a level deeper.
1035 let mut with_noise = NVIM_LUA_RANGES.to_vec();
1036 with_noise.push((5, 5)); // single row → dropped
1037 with_noise.push((9, 8)); // inverted → dropped
1038 assert_eq!(
1039 closed_at(&with_noise, 1),
1040 closed_at(NVIM_LUA_RANGES, 1),
1041 "ranges that never become folds must not count as a nesting level"
1042 );
1043 }
1044
1045 #[test]
1046 fn set_auto_folds_manual_folds_do_not_add_a_nesting_level() {
1047 // A `zf` fold wrapping the whole file must not make every auto fold
1048 // one level deeper — the auto set's levels are its own.
1049 let mut buf = View::from_str(&"x\n".repeat(26));
1050 buf.add_fold(0, 25, false);
1051 buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1052 let closed: Vec<(usize, usize)> = buf
1053 .folds()
1054 .iter()
1055 .filter(|f| f.auto_generated && f.closed)
1056 .map(|f| (f.start_row, f.end_row))
1057 .collect();
1058 assert_eq!(
1059 closed,
1060 vec![(3, 14), (4, 7), (10, 13), (19, 21)],
1061 "an enclosing manual fold must not shift auto fold levels"
1062 );
1063 }
1064
1065 #[test]
1066 fn set_auto_folds_at_a_level_settles_without_bumping_generations() {
1067 // The mixed open/closed set a non-zero `foldlevelstart` produces has
1068 // to compare equal on the next pass, or the caller's once-per-edit
1069 // guard fires every frame.
1070 let mut buf = View::from_str(&"x\n".repeat(26));
1071 buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1072 let fg = buf.fold_gen();
1073 let dg = buf.dirty_gen();
1074 for _ in 0..5 {
1075 buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1076 }
1077 assert_eq!(
1078 buf.fold_gen(),
1079 fg,
1080 "unchanged fold set must not bump fold_gen"
1081 );
1082 assert_eq!(
1083 buf.dirty_gen(),
1084 dg,
1085 "unchanged fold set must not bump dirty_gen"
1086 );
1087 }
1088
1089 // ── row-delta shifting (audit-r2 fix 1) ───────────────────────────────
1090
1091 fn fold(s: usize, e: usize) -> super::Fold {
1092 super::Fold {
1093 start_row: s,
1094 end_row: e,
1095 closed: true,
1096 auto_generated: false,
1097 }
1098 }
1099
1100 #[test]
1101 fn shift_fold_insert_above_shifts_down() {
1102 // 10-line file, fold at rows 4..6, insert one row at row 0
1103 // (`ggO x<Esc>`): vim shifts the fold to 5..7.
1104 let f = fold(4, 6);
1105 let shifted = super::shift_fold(f, 0, 0, 1, 1).unwrap();
1106 assert_eq!((shifted.start_row, shifted.end_row), (5, 7));
1107 }
1108
1109 #[test]
1110 fn shift_fold_delete_above_shifts_up() {
1111 // Fold at rows 4..6, one row deleted above at row 0.
1112 let f = fold(4, 6);
1113 let shifted = super::shift_fold(f, 0, 1, 1, -1).unwrap();
1114 assert_eq!((shifted.start_row, shifted.end_row), (3, 5));
1115 }
1116
1117 #[test]
1118 fn shift_fold_delete_fully_overlapping_drops() {
1119 // Fold at rows 4..6, deletion covers rows 4..6 entirely.
1120 let f = fold(4, 6);
1121 assert!(super::shift_fold(f, 4, 7, 7, -3).is_none());
1122 }
1123
1124 #[test]
1125 fn shift_fold_delete_overlapping_tail_clips() {
1126 // Fold at rows 4..6, deletion of rows 6..8 (tail only) clips the
1127 // fold to end at the last surviving row.
1128 let f = fold(4, 6);
1129 let shifted = super::shift_fold(f, 6, 9, 9, -3).unwrap();
1130 assert_eq!((shifted.start_row, shifted.end_row), (4, 5));
1131 }
1132
1133 #[test]
1134 fn shift_fold_delete_overlapping_head_clips() {
1135 // Fold at rows 4..6, deletion of rows 3..4 (head only) clips the
1136 // fold to start where the surviving tail now sits.
1137 let f = fold(4, 6);
1138 let shifted = super::shift_fold(f, 3, 5, 5, -2).unwrap();
1139 assert_eq!((shifted.start_row, shifted.end_row), (3, 4));
1140 }
1141
1142 #[test]
1143 fn shift_fold_edit_inside_grows_on_insert() {
1144 // Fold at rows 4..6, a line inserted at row 5 (strictly inside):
1145 // vim grows the fold's end, leaves the start alone.
1146 let f = fold(4, 6);
1147 let shifted = super::shift_fold(f, 5, 5, 6, 1).unwrap();
1148 assert_eq!((shifted.start_row, shifted.end_row), (4, 7));
1149 }
1150
1151 #[test]
1152 fn shift_fold_edit_inside_shrinks_on_delete() {
1153 // Fold at rows 4..8, rows 5..6 deleted (strictly inside): the fold
1154 // shrinks around the deletion instead of clipping or dropping.
1155 let f = fold(4, 8);
1156 let shifted = super::shift_fold(f, 5, 7, 7, -2).unwrap();
1157 assert_eq!((shifted.start_row, shifted.end_row), (4, 6));
1158 }
1159
1160 #[test]
1161 fn shift_fold_unaffected_when_entirely_before_edit() {
1162 let f = fold(1, 2);
1163 let shifted = super::shift_fold(f, 10, 11, 11, 1).unwrap();
1164 assert_eq!((shifted.start_row, shifted.end_row), (1, 2));
1165 }
1166
1167 #[test]
1168 fn shift_fold_zero_delta_is_noop() {
1169 let f = fold(4, 6);
1170 let shifted = super::shift_fold(f, 0, 0, 0, 0).unwrap();
1171 assert_eq!(shifted, f);
1172 }
1173
1174 #[test]
1175 fn shift_folds_after_edit_shifts_vec_in_place() {
1176 let mut folds = vec![fold(4, 6), fold(1, 2)];
1177 // Insert one row at row 0: both folds shift down.
1178 super::shift_folds_after_edit(&mut folds, 0, 0, 1, 1);
1179 let ranges: Vec<(usize, usize)> = folds.iter().map(|f| (f.start_row, f.end_row)).collect();
1180 assert_eq!(ranges, vec![(5, 7), (2, 3)]);
1181 }
1182
1183 #[test]
1184 fn shift_folds_after_edit_drops_fully_consumed() {
1185 let mut folds = vec![fold(4, 6)];
1186 super::shift_folds_after_edit(&mut folds, 4, 7, 7, -3);
1187 assert!(folds.is_empty());
1188 }
1189
1190 // ── Borrow-style accessors + fold generation (round-2 perf item 10) ───
1191
1192 #[test]
1193 fn with_folds_sees_the_same_data_as_folds() {
1194 let mut buf = b();
1195 // Empty case.
1196 assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
1197 assert!(!buf.has_folds());
1198
1199 buf.add_fold(0, 1, false);
1200 buf.add_fold(2, 3, true);
1201 assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
1202 assert!(buf.has_folds());
1203 // And the borrow-style scan agrees with the owning one row by row.
1204 for row in 0..buf.row_count() {
1205 assert_eq!(
1206 buf.with_folds(|f| f.iter().any(|f| f.hides(row))),
1207 buf.folds().iter().any(|f| f.hides(row)),
1208 "row {row}"
1209 );
1210 }
1211 }
1212
1213 #[test]
1214 fn fold_gen_bumps_on_every_mutator() {
1215 fn none(_: &mut View) {}
1216 fn open_fold(b: &mut View) {
1217 b.add_fold(1, 3, false);
1218 }
1219 fn closed_fold(b: &mut View) {
1220 b.add_fold(1, 3, true);
1221 }
1222 /// (name, setup, mutation-that-must-bump)
1223 type Case = (&'static str, fn(&mut View), fn(&mut View));
1224 let cases: [Case; 13] = [
1225 ("add_fold", none, |b| b.add_fold(1, 3, false)),
1226 ("set_auto_folds", none, |b| b.set_auto_folds(&[(1, 3)], 99)),
1227 ("remove_fold_at", open_fold, |b| {
1228 assert!(b.remove_fold_at(2));
1229 }),
1230 ("open_fold_at", closed_fold, |b| {
1231 assert!(b.open_fold_at(2));
1232 }),
1233 ("close_fold_at", open_fold, |b| {
1234 assert!(b.close_fold_at(2));
1235 }),
1236 ("toggle_fold_at", open_fold, |b| {
1237 assert!(b.toggle_fold_at(2));
1238 }),
1239 ("open_all_folds", closed_fold, View::open_all_folds),
1240 ("close_all_folds", open_fold, View::close_all_folds),
1241 ("clear_all_folds", open_fold, View::clear_all_folds),
1242 ("reveal_row", closed_fold, |b| {
1243 assert!(b.reveal_row(2));
1244 }),
1245 ("invalidate_folds_in_range", closed_fold, |b| {
1246 b.invalidate_folds_in_range(2, 2);
1247 }),
1248 ("rebase_folds", closed_fold, |b| b.rebase_folds(0, 0, 1, 1)),
1249 ("set_folds", none, |b| {
1250 b.set_folds(&[super::Fold {
1251 start_row: 1,
1252 end_row: 3,
1253 closed: true,
1254 auto_generated: false,
1255 }]);
1256 }),
1257 ];
1258 for (name, setup, mutate) in cases {
1259 let mut buf = b();
1260 setup(&mut buf);
1261 let before = buf.fold_gen();
1262 mutate(&mut buf);
1263 assert!(
1264 buf.fold_gen() > before,
1265 "{name} mutated the fold set and must bump fold_gen"
1266 );
1267 }
1268 }
1269
1270 #[test]
1271 fn fold_gen_is_stable_across_reads_and_plain_text_edits() {
1272 let mut buf = b();
1273 buf.add_fold(1, 3, true);
1274 let fg = buf.fold_gen();
1275 assert!(fg > 0);
1276
1277 // Pure reads.
1278 let _ = buf.folds();
1279 let _ = buf.with_folds(<[super::Fold]>::to_vec);
1280 let _ = buf.has_folds();
1281 let _ = buf.is_row_hidden(2);
1282 let _ = buf.fold_at_row(2);
1283 let _ = buf.next_visible_row(1);
1284 let _ = buf.prev_visible_row(4);
1285 assert_eq!(buf.fold_gen(), fg, "read-only queries must not bump");
1286
1287 // No-op mutators (nothing actually changes).
1288 buf.close_all_folds(); // already closed
1289 buf.add_fold(9, 9, true); // out of bounds → rejected
1290 buf.rebase_folds(0, 0, 1, 0); // delta == 0 → early return
1291 let same = buf.folds();
1292 buf.set_folds(&same); // identical set
1293 assert_eq!(buf.fold_gen(), fg, "no-op mutators must not bump");
1294
1295 // A plain text edit must not masquerade as a fold change — that is
1296 // the whole reason `fold_gen` is separate from `dirty_gen`.
1297 let dg = buf.dirty_gen();
1298 buf.apply_edit(crate::Edit::InsertChar {
1299 at: crate::Position { row: 0, col: 0 },
1300 ch: 'x',
1301 });
1302 assert_ne!(buf.dirty_gen(), dg, "text edit must bump dirty_gen");
1303 assert_eq!(buf.fold_gen(), fg, "text edit must not bump fold_gen");
1304 }
1305
1306 #[test]
1307 fn rebase_folds_shifts_buffer_fold_storage() {
1308 let mut buf = View::from_str("0\n1\n2\n3\n4\n5\n6\n7\n8\n9");
1309 buf.add_fold(4, 6, true);
1310 buf.rebase_folds(0, 0, 1, 1);
1311 let folds = buf.folds();
1312 assert_eq!(folds.len(), 1);
1313 assert_eq!((folds[0].start_row, folds[0].end_row), (5, 7));
1314 }
1315}