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