1use super::{App, Fold};
33
34impl App {
35 pub fn recompute_folds(&mut self) {
56 self.mutate_editor(|e| e.recompute_folds());
58 }
59
60 pub fn line_inside_closed_fold(&self, line: u32) -> bool {
74 #[cfg(test)]
80 {
81 self.editor.line_inside_closed_fold(line)
82 }
83 #[cfg(not(test))]
84 {
85 let ad = self.ad();
86 if !ad.option_cache.foldenable {
87 return false;
88 }
89 ad.folds
90 .iter()
91 .any(|f| f.closed && line > f.start_line && line <= f.end_line)
92 }
93 }
94
95 pub fn fold_start_at(&self, line: u32) -> Option<Fold> {
102 #[cfg(test)]
105 {
106 self.editor.fold_start_at(line).copied()
107 }
108 #[cfg(not(test))]
109 {
110 let ad = self.ad();
111 if !ad.option_cache.foldenable {
112 return None;
113 }
114 ad.folds
115 .iter()
116 .find(|f| f.closed && f.start_line == line)
117 .copied()
118 }
119 }
120
121 pub fn fold_start_at_any(&self, line: u32) -> Option<Fold> {
126 #[cfg(test)]
129 {
130 self.editor.fold_start_at_any(line).copied()
131 }
132 #[cfg(not(test))]
133 {
134 let ad = self.ad();
135 if !ad.option_cache.foldenable {
136 return None;
137 }
138 ad.folds.iter().find(|f| f.start_line == line).copied()
139 }
140 }
141
142 pub(super) fn snap_cursor_past_closed_folds(&mut self, prev_line: u32) {
148 self.mutate_editor(move |e| e.snap_cursor_past_closed_folds(prev_line));
150 }
151
152 pub fn auto_open_folds_at_cursor(&mut self) {
158 self.mutate_editor(|e| e.auto_open_folds_at_cursor());
160 }
161}
162
163pub(super) fn compute_fold_hash(folds: &[Fold]) -> u64 {
169 use std::hash::{DefaultHasher, Hash, Hasher};
170 let mut h = DefaultHasher::new();
171 folds.len().hash(&mut h);
172 for f in folds {
173 f.start_line.hash(&mut h);
174 f.end_line.hash(&mut h);
175 f.closed.hash(&mut h);
176 }
177 h.finish()
178}
179
180#[cfg(test)]
186mod tests {
187 #![allow(clippy::unwrap_used, clippy::panic)]
188
189 use super::compute_fold_hash;
190 use crate::app::test_helpers::{
191 app_with, attach_test_syntax, invoke_motion, press, press_chars,
192 };
193 use crate::app::*;
194 use lattice_grammar::{ModalState, VisualKind};
195 use lattice_protocol::edit::Edit;
196 use lattice_protocol::position::Position;
197
198 #[tokio::test(flavor = "multi_thread")]
221 async fn a_cross_file_jump_reveals_the_line_it_lands_on() {
222 let dir = crate::app::test_helpers::unique_tempdir();
223 std::fs::create_dir_all(&dir).unwrap();
224 let target = dir.join("target.rs");
225 std::fs::write(
228 &target,
229 "fn outer() {\n let a = 1;\n if a > 0 {\n let deep = 2;\n }\n}\n",
230 )
231 .unwrap();
232
233 let mut a = app_with("elsewhere\n", 20);
234 a.mutate_editor(|e| {
235 e.handle_effect(lattice_grammar::Effect::SetOption {
236 spec: "foldmethod=syntax".to_string(),
237 });
238 e.handle_effect(lattice_grammar::Effect::SetOption {
241 spec: "foldlevel=0".to_string(),
242 });
243 });
244
245 let deep = Position::new(3, 8);
246 a.apply_effect(lattice_grammar::Effect::OpenBufferAt {
247 path: Some(target.clone()),
248 position: deep,
249 force: false,
250 content: None,
251 activate_minor: None,
252 });
253
254 let folds = a.editor.folds.clone();
255 assert!(
256 !folds.is_empty(),
257 "precondition: the target file has syntax folds; without any, this \
258 test would pass on the broken code too"
259 );
260 assert!(
261 folds
262 .iter()
263 .any(|f| deep.line >= f.start_line && deep.line <= f.end_line),
264 "precondition: the landing line is inside a fold, got {folds:?}"
265 );
266 assert!(
267 !folds
268 .iter()
269 .any(|f| f.closed && deep.line >= f.start_line && deep.line <= f.end_line),
270 "the jump revealed its target — no closed fold still hides it, got {folds:?}"
271 );
272 }
273
274 #[test]
278 fn open_buffer_at_seeds_a_new_file_and_activates_its_minor() {
279 let dir = std::env::temp_dir().join(format!(
280 "lattice-tui-cd2-{}-{}",
281 std::process::id(),
282 std::time::SystemTime::now()
283 .duration_since(std::time::UNIX_EPOCH)
284 .map(|d| d.as_nanos())
285 .unwrap_or(0)
286 ));
287 std::fs::create_dir_all(&dir).unwrap();
288 let path = dir.join("draft.org");
289
290 let mut a = app_with("elsewhere\n", 20);
291 a.apply_effect(lattice_grammar::Effect::OpenBufferAt {
292 path: Some(path.clone()),
293 position: Position::new(0, 7),
294 force: false,
295 content: Some("* TODO thing\n".to_string()),
296 activate_minor: Some("read-only-mode".to_string()),
297 });
298
299 assert_eq!(a.editor.document.snapshot().text(), "* TODO thing\n");
300 assert_eq!(a.editor.cursor, Position::new(0, 7));
301 let id = a.editor.active_pane_buffer_id();
302 assert!(
303 a.editor
304 .active_modes
305 .get(&id)
306 .is_some_and(|m| m.is_active(lattice_mode::ModeId::new("read-only-mode"))),
307 "the minor rides the open"
308 );
309 assert!(!path.exists(), "nothing written before :w");
310 let _ = std::fs::remove_dir_all(&dir);
311 }
312
313 #[test]
316 fn fold_hash_distinguishes_closed_vs_open() {
317 let f_open = Fold {
318 start_line: 0,
319 end_line: 2,
320 closed: false,
321 identity: None,
322 };
323 let f_closed = Fold {
324 closed: true,
325 ..f_open
326 };
327 assert_ne!(compute_fold_hash(&[f_open]), compute_fold_hash(&[f_closed]));
328 }
329
330 #[test]
331 fn fold_hash_distinguishes_different_ranges() {
332 let f1 = Fold {
333 start_line: 0,
334 end_line: 2,
335 closed: false,
336 identity: None,
337 };
338 let f2 = Fold {
339 start_line: 0,
340 end_line: 3,
341 closed: false,
342 identity: None,
343 };
344 assert_ne!(compute_fold_hash(&[f1]), compute_fold_hash(&[f2]));
345 }
346
347 #[test]
348 fn fold_hash_ignores_identity() {
349 let f1 = Fold {
354 start_line: 0,
355 end_line: 2,
356 closed: false,
357 identity: Some(42),
358 };
359 let f2 = Fold {
360 identity: Some(99),
361 ..f1
362 };
363 assert_eq!(compute_fold_hash(&[f1]), compute_fold_hash(&[f2]));
364 }
365
366 #[test]
371 fn zf_from_visual_creates_a_closed_fold() {
372 let mut a = app_with("a\nb\nc\nd\ne", 10);
373 a.apply(Action::EnterVisual(VisualKind::Linewise));
374 a.apply(invoke_motion(a.editor.builtins.line_down));
375 a.apply(invoke_motion(a.editor.builtins.line_down));
376 a.apply(Action::CreateFoldFromVisual);
378 assert_eq!(a.editor.folds.len(), 1);
379 let fold = &a.editor.folds[0];
380 assert_eq!(fold.start_line, 0);
381 assert_eq!(fold.end_line, 2);
382 assert!(fold.closed);
383 assert_eq!(a.editor.modal, ModalState::Normal);
385 }
386
387 #[test]
388 fn zf_outside_visual_emits_error() {
389 let mut a = app_with("hello", 10);
390 a.apply(Action::CreateFoldFromVisual);
391 let msg = a.editor.last_message.as_ref().unwrap();
392 assert_eq!(msg.level, EchoLevel::Error);
393 }
394
395 #[test]
400 fn zf_j_folds_two_lines() {
401 let mut a = app_with("a\nb\nc\nd\ne", 10);
402 press_chars(&mut a, "zfj");
403 assert_eq!(a.editor.folds.len(), 1, "folds: {:?}", a.editor.folds);
404 let f = &a.editor.folds[0];
405 assert_eq!((f.start_line, f.end_line, f.closed), (0, 1, true));
406 }
407
408 #[test]
416 fn zf_k_folds_upward() {
417 let mut a = app_with("a\nb\nc\nd\ne", 10);
418 a.editor.cursor = Position::new(2, 0);
419 press_chars(&mut a, "zfk");
420 assert_eq!(a.editor.folds.len(), 1, "folds: {:?}", a.editor.folds);
421 let f = &a.editor.folds[0];
422 assert_eq!((f.start_line, f.end_line, f.closed), (1, 2, true));
423 }
424
425 #[test]
427 fn zf_ip_folds_a_paragraph() {
428 let mut a = app_with("a\nb\n\nc\nd", 10);
429 press_chars(&mut a, "zfip");
430 assert_eq!(a.editor.folds.len(), 1, "folds: {:?}", a.editor.folds);
431 let f = &a.editor.folds[0];
432 assert_eq!((f.start_line, f.end_line), (0, 1));
433 }
434
435 #[test]
439 fn visual_zf_folds_whole_lines_and_ends_visual() {
440 let mut a = app_with("abc\ndef\nghi", 10);
441 a.editor.cursor = Position::new(0, 1);
442 press_chars(&mut a, "vjzf");
443 assert_eq!(a.editor.folds.len(), 1, "folds: {:?}", a.editor.folds);
444 let f = &a.editor.folds[0];
445 assert_eq!((f.start_line, f.end_line, f.closed), (0, 1, true));
446 assert_eq!(a.editor.modal, ModalState::Normal);
447 }
448
449 #[test]
451 fn zf_alone_waits_for_a_motion() {
452 let mut a = app_with("a\nb\nc", 10);
453 press_chars(&mut a, "zf");
454 assert!(a.editor.folds.is_empty());
455 assert_eq!(a.editor.partial_chord.len(), 2, "`zf` is a pending prefix");
456 }
457
458 #[test]
461 fn zff_is_a_prefix_not_a_doubled_operator() {
462 let a = app_with("a", 10);
463 let path = [
464 crate::chord::KeyChord::char('z'),
465 crate::chord::KeyChord::char('f'),
466 crate::chord::KeyChord::char('f'),
467 ];
468 assert!(
469 matches!(
470 a.editor
471 .keymap
472 .lookup(crate::keymap::BindingMode::Normal, &path),
473 crate::keymap_trie::LookupResult::Partial
474 ),
475 "`zff` must wait for the find-char target"
476 );
477 }
478
479 #[test]
482 fn ctrl_f_in_visual_pages_and_grows_the_selection() {
483 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
484 let text: String = (0..80).map(|n| format!("line {n}\n")).collect();
485 let mut a = app_with(&text, 10);
486 press_chars(&mut a, "v");
487 press(
488 &mut a,
489 KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL),
490 );
491 assert!(matches!(a.editor.modal, ModalState::Visual(_)));
492 assert!(a.editor.cursor.line > 0, "<C-f> pages the cursor down");
493 assert_eq!(
494 a.editor.visual_anchor.map(|p| p.line),
495 Some(0),
496 "the anchor stays, so the selection grows"
497 );
498 }
499
500 #[test]
503 fn ctrl_f_in_select_pages_rather_than_overtyping() {
504 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
505 let text: String = (0..80).map(|n| format!("line {n}\n")).collect();
506 let mut a = app_with(&text, 10);
507 press_chars(&mut a, "gh");
508 assert!(
509 matches!(a.editor.modal, ModalState::Select(_)),
510 "test premise"
511 );
512 press(
513 &mut a,
514 KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL),
515 );
516 assert!(matches!(a.editor.modal, ModalState::Select(_)));
517 assert!(a.editor.cursor.line > 0);
518 assert_eq!(a.editor.document.snapshot().buffer.as_string(), text);
519 }
520
521 #[test]
523 fn z_in_select_still_overtypes() {
524 let mut a = app_with("abc", 10);
525 press_chars(&mut a, "gh");
526 assert!(
527 matches!(a.editor.modal, ModalState::Select(_)),
528 "test premise"
529 );
530 press_chars(&mut a, "z");
531 assert!(
532 a.editor
533 .document
534 .snapshot()
535 .buffer
536 .as_string()
537 .starts_with('z')
538 );
539 assert_eq!(a.editor.modal, ModalState::Insert);
540 }
541
542 #[test]
544 fn zz_in_visual_scrolls_without_touching_the_selection() {
545 let text: String = (0..80).map(|n| format!("line {n}\n")).collect();
546 let mut a = app_with(&text, 10);
547 a.editor.cursor = Position::new(30, 0);
548 press_chars(&mut a, "vj");
549 let (anchor, cursor) = (a.editor.visual_anchor, a.editor.cursor);
550 press_chars(&mut a, "zz");
551 assert!(matches!(a.editor.modal, ModalState::Visual(_)));
552 assert_eq!(a.editor.visual_anchor, anchor);
553 assert_eq!(a.editor.cursor, cursor);
554 }
555
556 #[test]
563 fn a_count_repeats_zj() {
564 let text: String = (1..=14).map(|n| format!("line {n}\n")).collect();
565 let mut a = app_with(&text, 20);
566 for (start_line, end_line) in [(3, 5), (8, 9)] {
567 a.editor.folds.push(Fold {
568 start_line,
569 end_line,
570 closed: false,
571 identity: None,
572 });
573 }
574 a.editor.cursor = Position::new(0, 2);
575 press_chars(&mut a, "2zj");
576 assert_eq!(a.editor.cursor, Position::new(8, 0));
577 }
578
579 #[test]
582 fn dzj_keeps_the_newline_before_the_next_fold() {
583 let text: String = (1..=14).map(|n| format!("line {n}\n")).collect();
584 let mut a = app_with(&text, 20);
585 for (start_line, end_line) in [(3, 5), (8, 9)] {
586 a.editor.folds.push(Fold {
587 start_line,
588 end_line,
589 closed: false,
590 identity: None,
591 });
592 }
593 a.editor.cursor = Position::new(0, 2);
594 press_chars(&mut a, "dzj");
595 let after = a.editor.document.snapshot().buffer.as_string();
596 assert_eq!(after.lines().next(), Some("li"), "{after:?}");
597 assert_eq!(after.lines().nth(1), Some("line 4"), "{after:?}");
598 assert_eq!(after.lines().count(), 12);
599 }
600
601 #[test]
602 fn dzk_deletes_back_to_the_previous_fold_end() {
603 let text: String = (1..=14).map(|n| format!("line {n}\n")).collect();
604 let mut a = app_with(&text, 20);
605 for (start_line, end_line) in [(3, 5), (8, 9)] {
606 a.editor.folds.push(Fold {
607 start_line,
608 end_line,
609 closed: false,
610 identity: None,
611 });
612 }
613 a.editor.cursor = Position::new(11, 2);
614 press_chars(&mut a, "dzk");
615 let after = a.editor.document.snapshot().buffer.as_string();
616 assert_eq!(
617 after.lines().nth(9),
618 Some("ne 12"),
619 "lines 10-12 join at the deleted span: {after:?}"
620 );
621 assert_eq!(after.lines().count(), 12);
622 assert_eq!(a.editor.cursor, Position::new(9, 0));
623 }
624
625 #[test]
626 fn zo_opens_fold_at_cursor() {
627 let mut a = app_with("a\nb\nc", 10);
628 a.editor.folds.push(Fold {
629 start_line: 0,
630 end_line: 2,
631 closed: true,
632 identity: None,
633 });
634 a.apply(Action::OpenFoldAtCursor);
635 assert!(!a.editor.folds[0].closed);
636 }
637
638 #[test]
639 fn zc_closes_fold_at_cursor() {
640 let mut a = app_with("a\nb\nc", 10);
641 a.editor.folds.push(Fold {
642 start_line: 0,
643 end_line: 2,
644 closed: false,
645 identity: None,
646 });
647 a.apply(Action::CloseFoldAtCursor);
648 assert!(a.editor.folds[0].closed);
649 }
650
651 #[test]
652 fn za_toggles_fold_at_cursor() {
653 let mut a = app_with("a\nb\nc", 10);
654 a.editor.folds.push(Fold {
655 start_line: 0,
656 end_line: 2,
657 closed: false,
658 identity: None,
659 });
660 a.apply(Action::ToggleFoldAtCursor);
661 assert!(a.editor.folds[0].closed);
662 a.apply(Action::ToggleFoldAtCursor);
663 assert!(!a.editor.folds[0].closed);
664 }
665
666 #[test]
667 fn capital_zr_opens_all_folds() {
668 let mut a = app_with("a\nb\nc\nd", 10);
669 a.editor.folds.push(Fold {
670 start_line: 0,
671 end_line: 1,
672 closed: true,
673 identity: None,
674 });
675 a.editor.folds.push(Fold {
676 start_line: 2,
677 end_line: 3,
678 closed: true,
679 identity: None,
680 });
681 a.apply(Action::OpenAllFolds);
682 assert!(a.editor.folds.iter().all(|f| !f.closed));
683 }
684
685 #[test]
686 fn capital_zm_closes_all_folds() {
687 let mut a = app_with("a\nb\nc\nd", 10);
688 a.editor.folds.push(Fold {
689 start_line: 0,
690 end_line: 1,
691 closed: false,
692 identity: None,
693 });
694 a.editor.folds.push(Fold {
695 start_line: 2,
696 end_line: 3,
697 closed: false,
698 identity: None,
699 });
700 a.apply(Action::CloseAllFolds);
701 assert!(a.editor.folds.iter().all(|f| f.closed));
702 }
703
704 #[test]
705 fn zd_deletes_fold_at_cursor() {
706 let mut a = app_with("a\nb\nc\nd", 10);
707 a.editor.folds.push(Fold {
708 start_line: 0,
709 end_line: 2,
710 closed: true,
711 identity: None,
712 });
713 a.editor.cursor = Position::new(1, 0);
714 a.apply(Action::DeleteFoldAtCursor);
715 assert!(a.editor.folds.is_empty());
716 }
717
718 fn nested_folds_app() -> App {
721 let mut a = app_with(&"x\n".repeat(12), 10);
724 a.editor.folds.push(Fold {
725 start_line: 0,
726 end_line: 10,
727 closed: false,
728 identity: None,
729 });
730 a.editor.folds.push(Fold {
731 start_line: 2,
732 end_line: 8,
733 closed: false,
734 identity: None,
735 });
736 a.editor.cursor = Position::new(4, 0);
737 a
738 }
739
740 #[test]
741 fn zc_closes_innermost_open_fold_first() {
742 let mut a = nested_folds_app();
743 a.apply(Action::CloseFoldAtCursor);
744 let inner = a.editor.folds.iter().find(|f| f.start_line == 2).unwrap();
745 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
746 assert!(inner.closed, "inner should close first");
747 assert!(!outer.closed, "outer should remain open until next zc");
748 }
749
750 #[test]
751 fn second_zc_closes_outer_fold() {
752 let mut a = nested_folds_app();
753 a.apply(Action::CloseFoldAtCursor); a.apply(Action::CloseFoldAtCursor); let inner = a.editor.folds.iter().find(|f| f.start_line == 2).unwrap();
756 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
757 assert!(inner.closed);
758 assert!(outer.closed);
759 }
760
761 #[test]
762 fn zo_opens_outermost_closed_fold_first() {
763 let mut a = nested_folds_app();
764 for f in a.editor.folds.iter_mut() {
766 f.closed = true;
767 }
768 a.apply(Action::OpenFoldAtCursor);
769 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
770 let inner = a.editor.folds.iter().find(|f| f.start_line == 2).unwrap();
771 assert!(!outer.closed, "outer should open first");
772 assert!(inner.closed, "inner should remain closed until next zo");
773 }
774
775 #[test]
776 fn za_toggles_to_open_when_any_fold_closed_then_close_when_all_open() {
777 let mut a = nested_folds_app();
778 a.editor.folds[0].closed = true;
780 a.apply(Action::ToggleFoldAtCursor);
782 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
783 let inner = a.editor.folds.iter().find(|f| f.start_line == 2).unwrap();
784 assert!(!outer.closed);
785 assert!(!inner.closed);
786 a.apply(Action::ToggleFoldAtCursor);
788 let inner = a.editor.folds.iter().find(|f| f.start_line == 2).unwrap();
789 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
790 assert!(inner.closed);
791 assert!(!outer.closed);
792 }
793
794 #[test]
795 fn zc_with_all_folds_closed_emits_e490() {
796 let mut a = nested_folds_app();
797 for f in a.editor.folds.iter_mut() {
798 f.closed = true;
799 }
800 a.apply(Action::CloseFoldAtCursor);
801 assert!(a.editor.folds.iter().all(|f| f.closed));
803 let msg = a
805 .editor
806 .last_message
807 .as_ref()
808 .expect("message")
809 .text
810 .clone();
811 assert!(msg.contains("E490"), "expected E490, got {msg:?}");
812 }
813
814 #[test]
815 fn zd_removes_innermost_only() {
816 let mut a = nested_folds_app();
817 a.apply(Action::DeleteFoldAtCursor);
818 assert!(a.editor.folds.iter().any(|f| f.start_line == 0));
820 assert!(!a.editor.folds.iter().any(|f| f.start_line == 2));
821 }
822
823 #[test]
826 fn line_down_from_closed_fold_heading_skips_to_after_fold() {
827 let mut a = app_with(&"x\n".repeat(12), 10);
830 a.editor.folds.push(Fold {
831 start_line: 1,
832 end_line: 4,
833 closed: true,
834 identity: None,
835 });
836 a.editor.cursor = Position::new(1, 0);
837 a.apply(invoke_motion(a.editor.builtins.line_down));
838 assert_eq!(
839 a.editor.cursor.line, 5,
840 "j from closed-fold heading must skip to fold.end_line + 1"
841 );
842 }
843
844 #[test]
845 fn line_up_into_closed_fold_snaps_to_heading() {
846 let mut a = app_with(&"x\n".repeat(12), 10);
849 a.editor.folds.push(Fold {
850 start_line: 1,
851 end_line: 4,
852 closed: true,
853 identity: None,
854 });
855 a.editor.cursor = Position::new(5, 0);
856 a.apply(invoke_motion(a.editor.builtins.line_up));
857 assert_eq!(
858 a.editor.cursor.line, 1,
859 "k into a closed fold must snap to its heading line"
860 );
861 }
862
863 #[test]
864 fn linear_j_into_open_fold_does_not_skip() {
865 let mut a = app_with(&"x\n".repeat(12), 10);
867 a.editor.folds.push(Fold {
868 start_line: 1,
869 end_line: 4,
870 closed: false,
871 identity: None,
872 });
873 a.editor.cursor = Position::new(1, 0);
874 a.apply(invoke_motion(a.editor.builtins.line_down));
875 assert_eq!(a.editor.cursor.line, 2);
876 }
877
878 #[test]
879 fn linear_motion_with_nofoldenable_does_not_skip() {
880 let mut a = app_with(&"x\n".repeat(12), 10);
883 a.editor.folds.push(Fold {
884 start_line: 1,
885 end_line: 4,
886 closed: true,
887 identity: None,
888 });
889 a.set_foldenable_for_test(false);
890 a.editor.cursor = Position::new(1, 0);
891 a.apply(invoke_motion(a.editor.builtins.line_down));
892 assert_eq!(a.editor.cursor.line, 2);
893 }
894
895 fn line_down_lands_on_next_fold_heading_when_consecutive() {
896 let mut a = app_with(&"x\n".repeat(12), 10);
903 a.editor.folds.push(Fold {
904 start_line: 1,
905 end_line: 3,
906 closed: true,
907 identity: None,
908 });
909 a.editor.folds.push(Fold {
910 start_line: 4,
911 end_line: 6,
912 closed: true,
913 identity: None,
914 });
915 a.editor.folds.push(Fold {
916 start_line: 7,
917 end_line: 9,
918 closed: true,
919 identity: None,
920 });
921 a.editor.cursor = Position::new(1, 0);
922 a.apply(invoke_motion(a.editor.builtins.line_down));
923 assert_eq!(a.editor.cursor.line, 4, "first j → fold B heading");
924 a.apply(invoke_motion(a.editor.builtins.line_down));
925 assert_eq!(a.editor.cursor.line, 7, "second j → fold C heading");
926 a.apply(invoke_motion(a.editor.builtins.line_down));
927 assert_eq!(a.editor.cursor.line, 10, "third j → past fold C");
928 }
929
930 #[test]
931 fn line_down_skips_consecutive_closed_folds_in_one_keypress() {
932 line_down_lands_on_next_fold_heading_when_consecutive();
937 }
938
939 #[test]
942 fn word_forward_snaps_out_of_closed_fold_body() {
943 let src = "alpha bravo\n charlie delta\n echo foxtrot\nafter golf hotel\n";
948 let mut a = app_with(src, 10);
949 a.editor.folds.push(Fold {
950 start_line: 0,
951 end_line: 2,
952 closed: true,
953 identity: None,
954 });
955 a.editor.cursor = Position::new(0, 0);
956 a.apply(invoke_motion(a.editor.builtins.word_forward));
957 a.apply(invoke_motion(a.editor.builtins.word_forward));
961 assert!(
962 !a.line_inside_closed_fold(a.editor.cursor.line),
963 "w must not leave cursor inside a hidden fold body \
964 (cursor.line = {})",
965 a.editor.cursor.line
966 );
967 }
968
969 #[test]
980 fn syntax_fold_zc_on_indented_let_with_if_else_reports_five_lines() {
981 let src = "fn outer() -> u32 {\n let len = if has_trailing_newline {\n bytes - 1\n } else {\n bytes\n };\n len\n}\n";
987 let mut a = app_with(src, 20);
988 attach_test_syntax(&mut a, lattice_syntax::Lang::Rust);
989 a.set_foldmethod_for_test(FoldMethod::Syntax);
990 a.recompute_folds();
991 eprintln!("FOLDS (indented let-if-else inside fn):");
994 for f in &a.editor.folds {
995 eprintln!(
996 " ({}, {}) span={} lines",
997 f.start_line,
998 f.end_line,
999 f.end_line - f.start_line + 1
1000 );
1001 }
1002 a.editor.cursor = Position::new(1, 0);
1007 a.apply(Action::CloseFoldAtCursor);
1008 let fold = a
1009 .fold_start_at(1)
1010 .expect("a closed fold should start at line 1");
1011 let count = fold.end_line - fold.start_line + 1;
1012 assert_eq!(
1013 count, 5,
1014 "indented if/else fold must span 5 lines (got {count}; fold = {fold:?}; all = {:?})",
1015 a.editor.folds
1016 );
1017 }
1018
1019 #[test]
1020 fn syntax_fold_zc_on_let_with_if_else_reports_five_lines() {
1021 let src = "let len = if cond {\n bytes - 1\n} else {\n bytes\n}\n";
1027 let mut a = app_with(src, 10);
1028 attach_test_syntax(&mut a, lattice_syntax::Lang::Rust);
1029 a.set_foldmethod_for_test(FoldMethod::Syntax);
1030 a.recompute_folds();
1031 a.editor.cursor = Position::new(0, 0);
1035 a.apply(Action::CloseFoldAtCursor);
1036 let fold = a
1037 .fold_start_at(0)
1038 .expect("a closed fold should start at line 0 after zc");
1039 let count = fold.end_line - fold.start_line + 1;
1040 assert_eq!(
1041 count, 5,
1042 "fold at line 0 must span 5 lines (got {count}; fold = {fold:?})"
1043 );
1044 }
1045
1046 #[test]
1047 fn paragraph_motion_snaps_out_of_closed_fold_body() {
1048 let src = "alpha\n\n body line one\n body line two\n\nafter\n";
1051 let mut a = app_with(src, 10);
1052 a.editor.folds.push(Fold {
1053 start_line: 0,
1054 end_line: 4,
1055 closed: true,
1056 identity: None,
1057 });
1058 a.editor.cursor = Position::new(0, 0);
1059 a.apply(invoke_motion(a.editor.builtins.paragraph_forward));
1060 assert!(
1061 !a.line_inside_closed_fold(a.editor.cursor.line),
1062 "}} must not leave cursor inside a hidden fold body \
1063 (cursor.line = {})",
1064 a.editor.cursor.line
1065 );
1066 }
1067
1068 #[test]
1069 fn line_down_swallows_blanks_between_sibling_folds_for_zc_targeting() {
1070 let src =
1089 "impl B {\n fn a() {\n }\n\n fn b() {\n }\n\n fn c() {\n }\n}\n";
1090 let mut a = app_with(src, 20);
1091 a.editor.folds.push(Fold {
1093 start_line: 0,
1094 end_line: 9,
1095 closed: false,
1096 identity: None,
1097 });
1098 a.editor.folds.push(Fold {
1099 start_line: 1,
1100 end_line: 2,
1101 closed: true,
1102 identity: None,
1103 });
1104 a.editor.folds.push(Fold {
1105 start_line: 4,
1106 end_line: 5,
1107 closed: false,
1108 identity: None,
1109 });
1110 a.editor.folds.push(Fold {
1111 start_line: 7,
1112 end_line: 8,
1113 closed: false,
1114 identity: None,
1115 });
1116 a.editor.cursor = Position::new(1, 0);
1117 a.apply(invoke_motion(a.editor.builtins.line_down));
1120 assert_eq!(
1121 a.editor.cursor.line, 4,
1122 "j after fold A must skip the blank and land on fn b"
1123 );
1124 a.apply(Action::CloseFoldAtCursor);
1126 let fnb = a.editor.folds.iter().find(|f| f.start_line == 4).unwrap();
1127 assert!(fnb.closed, "zc on fn b heading closes fn b");
1128 a.apply(invoke_motion(a.editor.builtins.line_down));
1129 assert_eq!(
1130 a.editor.cursor.line, 7,
1131 "j after fold B must skip the blank and land on fn c"
1132 );
1133 a.apply(Action::CloseFoldAtCursor);
1135 let fnc = a.editor.folds.iter().find(|f| f.start_line == 7).unwrap();
1136 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
1137 assert!(fnc.closed, "zc on fn c heading closes fn c, not outer");
1138 assert!(
1139 !outer.closed,
1140 "outer impl must remain open through the sequence"
1141 );
1142 }
1143
1144 #[test]
1145 fn zc_on_sibling_fold_after_navigating_with_j_closes_sibling_not_parent() {
1146 let mut a = app_with(&"x\n".repeat(12), 10);
1151 a.editor.folds.push(Fold {
1152 start_line: 0,
1153 end_line: 10,
1154 closed: false,
1155 identity: None,
1156 });
1157 a.editor.folds.push(Fold {
1158 start_line: 1,
1159 end_line: 4,
1160 closed: true,
1161 identity: None,
1162 });
1163 a.editor.folds.push(Fold {
1164 start_line: 5,
1165 end_line: 9,
1166 closed: false,
1167 identity: None,
1168 });
1169 a.editor.cursor = Position::new(1, 0);
1170 a.apply(invoke_motion(a.editor.builtins.line_down));
1172 assert_eq!(
1173 a.editor.cursor.line, 5,
1174 "cursor should land on sibling, not interior"
1175 );
1176 a.apply(Action::CloseFoldAtCursor);
1178 let sibling = a.editor.folds.iter().find(|f| f.start_line == 5).unwrap();
1179 let outer = a.editor.folds.iter().find(|f| f.start_line == 0).unwrap();
1180 assert!(sibling.closed, "sibling should close, not the outer");
1181 assert!(!outer.closed, "outer must remain open");
1182 }
1183
1184 #[test]
1185 fn zj_jumps_to_next_fold_start() {
1186 let mut a = app_with("a\nb\nc\nd\ne\nf", 10);
1187 a.editor.folds.push(Fold {
1188 start_line: 2,
1189 end_line: 3,
1190 closed: false,
1191 identity: None,
1192 });
1193 a.editor.folds.push(Fold {
1194 start_line: 5,
1195 end_line: 5,
1196 closed: false,
1197 identity: None,
1198 });
1199 a.editor.cursor = Position::ZERO;
1200 a.apply(Action::GotoNextFold);
1201 assert_eq!(a.editor.cursor.line, 2);
1202 }
1203
1204 #[test]
1205 fn zk_jumps_to_previous_fold_end() {
1206 let mut a = app_with("a\nb\nc\nd\ne\nf", 10);
1207 a.editor.folds.push(Fold {
1208 start_line: 1,
1209 end_line: 2,
1210 closed: false,
1211 identity: None,
1212 });
1213 a.editor.cursor = Position::new(5, 0);
1214 a.apply(Action::GotoPrevFold);
1215 assert_eq!(a.editor.cursor.line, 2);
1216 }
1217
1218 #[test]
1219 fn zj_with_no_more_folds_emits_error() {
1220 let mut a = app_with("a\nb\nc", 10);
1221 a.apply(Action::GotoNextFold);
1222 let msg = a.editor.last_message.as_ref().unwrap();
1223 assert_eq!(msg.level, EchoLevel::Error);
1224 }
1225
1226 #[test]
1227 fn line_inside_closed_fold_returns_true_for_interior() {
1228 let mut a = app_with("a\nb\nc\nd", 10);
1229 a.editor.folds.push(Fold {
1230 start_line: 1,
1231 end_line: 3,
1232 closed: true,
1233 identity: None,
1234 });
1235 a.editor.publish_render_state();
1236 assert!(!a.line_inside_closed_fold(0));
1237 assert!(!a.line_inside_closed_fold(1));
1239 assert!(a.line_inside_closed_fold(2));
1240 assert!(a.line_inside_closed_fold(3));
1241 }
1242
1243 #[test]
1248 fn foldmethod_indent_populates_folds_from_indentation() {
1249 let mut a = app_with("def f():\n pass\n pass\n", 10);
1250 a.editor.set_command_line_text("set foldmethod=indent");
1251 a.editor.modal = ModalState::Command;
1252 a.apply(Action::CommandLineSubmit);
1253 assert_eq!(a.foldmethod(), FoldMethod::Indent);
1254 assert!(!a.editor.folds.is_empty());
1255 let f = a
1256 .editor
1257 .folds
1258 .iter()
1259 .find(|f| f.start_line == 0)
1260 .expect("fold");
1261 assert_eq!(f.end_line, 2);
1262 }
1263
1264 #[test]
1265 fn foldmethod_indent_preserves_closed_state_across_reparse() {
1266 let mut a = app_with("a:\n b\n c\n", 10);
1267 a.set_foldmethod_for_test(FoldMethod::Indent);
1268 a.recompute_folds();
1269 assert_eq!(a.editor.folds.len(), 1);
1270 a.editor.folds[0].closed = true;
1272 a.recompute_folds();
1274 assert!(a.editor.folds[0].closed);
1275 }
1276
1277 #[test]
1278 fn foldmethod_manual_default_does_not_recompute() {
1279 let mut a = app_with("def f():\n pass\n", 10);
1280 a.recompute_folds();
1281 assert!(a.editor.folds.is_empty());
1282 }
1283
1284 #[test]
1285 fn foldmethod_markdown_populates_folds_from_atx_headings() {
1286 let mut a = app_with("# H1\nbody\nmore body\n", 10);
1287 a.editor.set_command_line_text("set foldmethod=markdown");
1288 a.editor.modal = ModalState::Command;
1289 a.apply(Action::CommandLineSubmit);
1290 assert_eq!(a.foldmethod(), FoldMethod::Markdown);
1291 assert!(!a.editor.folds.is_empty());
1292 let f = a
1293 .editor
1294 .folds
1295 .iter()
1296 .find(|f| f.start_line == 0)
1297 .expect("fold");
1298 assert!(f.end_line >= 2);
1299 }
1300
1301 #[test]
1302 fn foldmethod_syntax_cascades_to_indent_when_no_md_extension() {
1303 let mut a = app_with("def f():\n pass\n pass\n", 10);
1306 a.editor.set_command_line_text("set foldmethod=syntax");
1307 a.editor.modal = ModalState::Command;
1308 a.apply(Action::CommandLineSubmit);
1309 assert_eq!(a.foldmethod(), FoldMethod::Syntax);
1310 assert!(
1311 a.editor
1312 .folds
1313 .iter()
1314 .any(|f| f.start_line == 0 && f.end_line == 2)
1315 );
1316 }
1317
1318 #[test]
1319 fn foldmethod_syntax_uses_tree_sitter_for_rust_buffer() {
1320 let mut a = app_with(
1324 "struct B {\n x: u8,\n}\n\nimpl B {\n fn n() -> Self {\n Self { x: 0 }\n }\n}\n",
1325 10,
1326 );
1327 attach_test_syntax(&mut a, lattice_syntax::Lang::Rust);
1330 a.editor.set_command_line_text("set foldmethod=syntax");
1331 a.editor.modal = ModalState::Command;
1332 a.apply(Action::CommandLineSubmit);
1333 assert_eq!(a.foldmethod(), FoldMethod::Syntax);
1334 assert!(
1336 a.editor
1337 .folds
1338 .iter()
1339 .any(|f| f.start_line == 0 && f.end_line >= 2),
1340 "expected struct fold from tree-sitter: {:?}",
1341 a.editor.folds
1342 );
1343 assert!(
1345 a.editor.folds.iter().any(|f| f.start_line == 4),
1346 "expected impl fold from tree-sitter: {:?}",
1347 a.editor.folds
1348 );
1349 }
1350
1351 #[test]
1352 fn foldmethod_indent_identity_preserves_closed_state_after_unrelated_insert() {
1353 let initial = "first:\n a\n b\nsecond:\n x\n y\n";
1359 let mut a = app_with(initial, 10);
1360 a.set_foldmethod_for_test(FoldMethod::Indent);
1361 a.recompute_folds();
1362 let second_idx = a
1364 .editor
1365 .folds
1366 .iter()
1367 .position(|f| f.start_line == 3)
1368 .expect("second: fold exists");
1369 a.editor.folds[second_idx].closed = true;
1370 a.apply_edit_blocking(Edit::insert(Position::new(2, 0), " extra\n"))
1372 .unwrap();
1373 a.recompute_folds();
1374 let new_second = a
1377 .editor
1378 .folds
1379 .iter()
1380 .find(|f| f.start_line == 4)
1381 .expect("second: fold survived insertion");
1382 assert!(
1383 new_second.closed,
1384 "closed-state should survive line shift via identity match"
1385 );
1386 }
1387
1388 #[test]
1389 fn foldmethod_rejects_unknown_value() {
1390 let mut a = app_with("a\n", 10);
1391 a.editor.set_command_line_text("set foldmethod=bogus");
1392 a.editor.modal = ModalState::Command;
1393 a.apply(Action::CommandLineSubmit);
1394 assert_eq!(a.foldmethod(), FoldMethod::Manual);
1395 assert!(a.editor.last_message.is_some());
1396 }
1397
1398 fn org_nested_app() -> App {
1403 let mut a = app_with(&"x\n".repeat(12), 20);
1404 for (s, e) in [(0u32, 10u32), (2, 4), (6, 8)] {
1405 a.editor.folds.push(Fold {
1406 start_line: s,
1407 end_line: e,
1408 closed: false,
1409 identity: None,
1410 });
1411 }
1412 a.editor.cursor = Position::new(0, 0);
1413 a
1414 }
1415
1416 fn closed_at(a: &App, start: u32) -> bool {
1417 a.editor
1418 .folds
1419 .iter()
1420 .find(|f| f.start_line == start)
1421 .unwrap()
1422 .closed
1423 }
1424
1425 #[test]
1426 fn org_local_cycle_folded_children_subtree() {
1427 let mut a = org_nested_app();
1428 a.apply(Action::CycleFoldAtCursor);
1430 assert!(closed_at(&a, 0), "FOLDED: parent closed");
1431 a.apply(Action::CycleFoldAtCursor);
1433 assert!(
1434 !closed_at(&a, 0) && closed_at(&a, 2) && closed_at(&a, 6),
1435 "CHILDREN: parent open, children closed"
1436 );
1437 a.apply(Action::CycleFoldAtCursor);
1439 assert!(
1440 !closed_at(&a, 0) && !closed_at(&a, 2) && !closed_at(&a, 6),
1441 "SUBTREE: all open"
1442 );
1443 a.apply(Action::CycleFoldAtCursor);
1445 assert!(closed_at(&a, 0), "cycles back to FOLDED");
1446 }
1447
1448 #[test]
1449 fn org_local_cycle_on_leaf_is_a_toggle() {
1450 let mut a = app_with(&"x\n".repeat(6), 10);
1452 a.editor.folds.push(Fold {
1453 start_line: 0,
1454 end_line: 4,
1455 closed: false,
1456 identity: None,
1457 });
1458 a.editor.cursor = Position::new(0, 0);
1459 a.apply(Action::CycleFoldAtCursor);
1460 assert!(a.editor.folds[0].closed, "leaf folds");
1461 a.apply(Action::CycleFoldAtCursor);
1462 assert!(!a.editor.folds[0].closed, "leaf re-opens");
1463 }
1464
1465 #[test]
1466 fn org_global_cycle_overview_contents_showall() {
1467 let mut a = org_nested_app(); a.apply(Action::CycleFoldsGlobal);
1470 assert!(
1471 a.editor.folds.iter().all(|f| f.closed),
1472 "OVERVIEW: all closed"
1473 );
1474 a.apply(Action::CycleFoldsGlobal);
1476 assert!(
1477 !closed_at(&a, 0) && closed_at(&a, 2) && closed_at(&a, 6),
1478 "CONTENTS: parent open, leaves closed"
1479 );
1480 a.apply(Action::CycleFoldsGlobal);
1482 assert!(
1483 a.editor.folds.iter().all(|f| !f.closed),
1484 "SHOW-ALL: all open"
1485 );
1486 a.apply(Action::CycleFoldsGlobal);
1488 assert!(
1489 a.editor.folds.iter().all(|f| f.closed),
1490 "cycles back to OVERVIEW"
1491 );
1492 }
1493
1494 #[test]
1495 fn org_cycle_off_a_fold_falls_back_to_global() {
1496 let mut a = app_with(&"x\n".repeat(10), 20);
1499 a.editor.folds.push(Fold {
1500 start_line: 2,
1501 end_line: 4,
1502 closed: false,
1503 identity: None,
1504 });
1505 a.editor.folds.push(Fold {
1506 start_line: 6,
1507 end_line: 8,
1508 closed: false,
1509 identity: None,
1510 });
1511 a.editor.cursor = Position::new(0, 0); a.apply(Action::CycleFoldAtCursor);
1513 assert!(
1515 a.editor.folds.iter().all(|f| f.closed),
1516 "off-fold z<Space> runs the global cycle (OVERVIEW)"
1517 );
1518 }
1519
1520 #[test]
1523 fn zp_goes_to_parent_heading() {
1524 let mut a = org_nested_app(); a.editor.cursor = Position::new(3, 0); a.apply(Action::GotoParentFold);
1527 assert_eq!(
1528 a.editor.cursor.line, 0,
1529 "zp from inside a child jumps to the parent heading"
1530 );
1531 a.apply(Action::GotoParentFold);
1533 assert_eq!(a.editor.cursor.line, 0, "zp at the top level stays put");
1534 let msg = a.editor.last_message.as_ref().unwrap().text.clone();
1535 assert!(
1536 msg.contains("no parent"),
1537 "expected no-parent note, got {msg:?}"
1538 );
1539 }
1540
1541 #[test]
1542 fn zp_climbs_one_level_at_a_time() {
1543 let mut a = app_with(&"x\n".repeat(22), 30);
1545 for (s, e) in [(0u32, 20u32), (5, 15), (8, 12)] {
1546 a.editor.folds.push(Fold {
1547 start_line: s,
1548 end_line: e,
1549 closed: false,
1550 identity: None,
1551 });
1552 }
1553 a.editor.cursor = Position::new(10, 0); a.apply(Action::GotoParentFold);
1555 assert_eq!(a.editor.cursor.line, 5, "### → ## (one level up)");
1556 a.apply(Action::GotoParentFold);
1557 assert_eq!(a.editor.cursor.line, 0, "## → # (one more level)");
1558 }
1559
1560 mod foldlevel_and_the_edit_path {
1569 use super::*;
1570 use crossterm::event::{KeyCode, KeyEvent};
1571
1572 fn org_shaped(text: &str) -> App {
1575 let mut a = app_with(text, 20);
1576 for cmd in ["set foldmethod=markdown", "set foldlevel=0"] {
1577 a.editor.set_command_line_text(cmd);
1578 a.editor.modal = ModalState::Command;
1579 a.apply(Action::CommandLineSubmit);
1580 }
1581 a
1582 }
1583
1584 fn fold_at(a: &App, start_line: u32) -> Fold {
1585 *a.editor
1586 .folds
1587 .iter()
1588 .find(|f| f.start_line == start_line)
1589 .unwrap_or_else(|| panic!("expected a fold starting at line {start_line}"))
1590 }
1591
1592 #[test]
1602 fn open_line_under_a_bare_heading_does_not_fold_it() {
1603 let mut a = org_shaped("# H1\n");
1604 assert!(a.editor.folds.is_empty(), "a bare heading has no fold yet");
1605
1606 a.editor.cursor = Position::new(0, 0);
1607 press(&mut a, KeyEvent::from(KeyCode::Char('o')));
1608 press(&mut a, KeyEvent::from(KeyCode::Char('x')));
1609
1610 assert!(
1611 !fold_at(&a, 0).closed,
1612 "the fold the user just opened a line inside must not snap shut"
1613 );
1614 }
1615
1616 #[test]
1620 fn a_nested_section_created_by_an_edit_arrives_open() {
1621 let mut a = org_shaped("# H1\nbody\n");
1622 a.editor.cursor = Position::new(1, 0);
1623 press(&mut a, KeyEvent::from(KeyCode::Char('o')));
1624 for ch in "## H2".chars() {
1625 press(&mut a, KeyEvent::from(KeyCode::Char(ch)));
1626 }
1627 press(&mut a, KeyEvent::from(KeyCode::Enter));
1628 press(&mut a, KeyEvent::from(KeyCode::Char('x')));
1629
1630 assert!(
1631 !fold_at(&a, 2).closed,
1632 "a sub-section the user just typed must not fold itself away"
1633 );
1634 }
1635
1636 #[test]
1641 fn structure_that_arrives_still_obeys_the_level() {
1642 let a = org_shaped("# H1\nbody\nmore\n");
1643 assert!(
1644 fold_at(&a, 0).closed,
1645 "a buffer that opens with structure in it still opens collapsed"
1646 );
1647 }
1648
1649 #[test]
1653 fn an_edit_leaves_the_other_sections_collapsed() {
1654 let mut a = org_shaped("# H1\nbody\n# H2\nbody\n");
1655 assert!(fold_at(&a, 2).closed, "H2 starts collapsed");
1656
1657 a.editor.cursor = Position::new(1, 0);
1658 press(&mut a, KeyEvent::from(KeyCode::Char('o')));
1659 press(&mut a, KeyEvent::from(KeyCode::Char('x')));
1660
1661 assert!(
1662 fold_at(&a, 3).closed,
1663 "H2 (now one line lower) keeps the closed state it was carried \
1664 over with — an edit in H1 is not a reason to expand it"
1665 );
1666 }
1667
1668 #[test]
1676 fn the_tick_does_not_re_close_what_the_edit_path_left_open() {
1677 let mut a = org_shaped("# H1\n");
1678 a.editor.cursor = Position::new(0, 0);
1679 press(&mut a, KeyEvent::from(KeyCode::Char('o')));
1680 press(&mut a, KeyEvent::from(KeyCode::Char('x')));
1681 assert!(!fold_at(&a, 0).closed);
1682
1683 a.editor.maybe_refold_after_async_population();
1684
1685 assert!(
1686 !fold_at(&a, 0).closed,
1687 "the idle tick must not undo what the edit path decided"
1688 );
1689 }
1690 }
1691
1692 mod zj_and_zk_step_between_visible_folds {
1701 use super::*;
1702
1703 fn nested_under_a_closed_fold() -> App {
1707 let mut a = app_with(&"x\n".repeat(45), 60);
1708 for (s, e, closed) in [(10u32, 30u32, true), (12, 20, false), (35, 40, false)] {
1709 a.editor.folds.push(Fold {
1710 start_line: s,
1711 end_line: e,
1712 closed,
1713 identity: None,
1714 });
1715 }
1716 a
1717 }
1718
1719 #[test]
1723 fn zj_skips_a_fold_hidden_inside_the_closed_one_it_is_on() {
1724 let mut a = nested_under_a_closed_fold();
1725 a.editor.cursor = Position::new(10, 0);
1726
1727 a.apply(Action::GotoNextFold);
1728
1729 assert_eq!(
1730 a.editor.cursor.line, 35,
1731 "the next VISIBLE fold is the sibling after the closed one, \
1732 not the sub-fold collapsed inside it"
1733 );
1734 }
1735
1736 #[test]
1740 fn zj_still_lands_on_a_closed_folds_own_head() {
1741 let mut a = nested_under_a_closed_fold();
1742 a.editor.cursor = Position::new(5, 0);
1743
1744 a.apply(Action::GotoNextFold);
1745
1746 assert_eq!(
1747 a.editor.cursor.line, 10,
1748 "the closed fold's head is visible"
1749 );
1750 }
1751
1752 #[test]
1755 fn zk_skips_a_fold_end_hidden_on_the_cursors_own_row() {
1756 let mut a = nested_under_a_closed_fold();
1757 a.editor.folds.push(Fold {
1760 start_line: 2,
1761 end_line: 5,
1762 closed: false,
1763 identity: None,
1764 });
1765 a.editor.cursor = Position::new(25, 0);
1766
1767 a.apply(Action::GotoPrevFold);
1768
1769 assert_eq!(
1770 a.editor.cursor.line, 5,
1771 "`[12..=20]` ends on the same row the cursor is already \
1772 displayed on, so the previous visible fold end is `[2..=5]`"
1773 );
1774 }
1775
1776 #[test]
1781 fn zk_still_enters_a_closed_fold_from_below() {
1782 let mut a = nested_under_a_closed_fold();
1783 a.editor.cursor = Position::new(32, 0);
1784
1785 a.apply(Action::GotoPrevFold);
1786
1787 assert_eq!(a.editor.cursor.line, 30, "the closed fold's end is a stop");
1788 }
1789
1790 #[test]
1793 fn nofoldenable_makes_every_edge_visible_again() {
1794 let mut a = nested_under_a_closed_fold();
1795 a.editor.set_command_line_text("set nofoldenable");
1796 a.editor.modal = ModalState::Command;
1797 a.apply(Action::CommandLineSubmit);
1798 a.editor.cursor = Position::new(10, 0);
1799
1800 a.apply(Action::GotoNextFold);
1801
1802 assert_eq!(
1803 a.editor.cursor.line, 12,
1804 "with folding disabled the nested fold is on screen, so it is \
1805 the next stop"
1806 );
1807 }
1808 }
1809}