//! Folding acceptance (Arc 6, Stage 1 — docs/folding-framing.md). //! //! Headless coverage of the fold engine over real grammars: the //! structural source (derived head line + closer-aware tail across brace //! and indentation grammars, wrapped signatures, and injection layers), //! the state-aware operations, the data-API validation, and the //! dispatch-layer command-path pre-edit unfold. The `FoldState` producer //! transitions are pinned in `src/semantic_render.rs` //! (`fold_state_producer_transitions`). use std::sync::Arc; use pmacs::buffer::{Buffer, BufferId, EditOp}; use pmacs::editor::EditorState; use pmacs::fold::{ CycleOutcome, FoldStore, close_at, cycle_at, fold_target_at, open_at, top_level_fold_targets, }; use pmacs::protocol::ByteRange; use pmacs::syntax::{ParseTreeBundle, ParseView, SyntaxRegistry, run_parse}; // --------------------------------------------------------------------------- // Harness // --------------------------------------------------------------------------- /// Parse `src` under `lang` synchronously into a resolved bundle (mirrors /// the crate-internal `parse_layered` with public APIs). fn parse(reg: &SyntaxRegistry, lang: &str, src: &[u8]) -> Arc { let language = reg.language(lang).expect("grammar loads"); let mut buf = Buffer::from_bytes(BufferId::next(), "doc", src); let view = ParseView::new(&buf, language, lang.to_owned()); let handle = view.handle(); let _ = buf.attach_view(Box::new(view)); let mut req = handle.make_request(); req.injection_aliases = reg.injection_alias_snapshot(); let bundle = run_parse(req).expect("parse succeeds"); reg.resolve_layer_queries(&bundle) } /// The byte offset of `needle`'s first occurrence in `src`. fn byte_of(src: &str, needle: &str) -> u64 { src.find(needle).expect("needle present") as u64 } /// The content-end byte (position of the terminating `\n`, or EOF) of the /// line containing `needle`'s first occurrence. fn line_content_end_of(src: &str, needle: &str) -> u64 { let idx = src.find(needle).expect("needle present"); let bytes = src.as_bytes(); let mut e = idx; while e < bytes.len() && bytes[e] != b'\n' { e += 1; } e as u64 } /// The text of the line containing byte `b`. fn line_text_at(src: &str, b: u64) -> &str { let bytes = src.as_bytes(); let b = (b as usize).min(bytes.len()); let start = bytes[..b] .iter() .rposition(|&c| c == b'\n') .map_or(0, |i| i + 1); let end = bytes[b..] .iter() .position(|&c| c == b'\n') .map_or(bytes.len(), |i| b + i); &src[start..end] } // --------------------------------------------------------------------------- // 1. Head line — both grammar shapes, wrapped headers (R2-1, R3-1). // --------------------------------------------------------------------------- #[test] fn head_line_rust_single_line_signature() { let reg = SyntaxRegistry::new(); let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n"; let bundle = parse(®, "rust", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "let x")).expect("foldable"); assert_eq!(r.start, line_content_end_of(src, "fn foo() {")); assert_eq!( line_text_at(src, r.start), "fn foo() {", "head line is the fn line" ); assert_eq!( line_text_at(src, r.start + 1), " let x = 1;", "first hidden line" ); } #[test] fn head_line_rust_wrapped_signature_keeps_signature_visible() { // R3-1: rustfmt puts `{` on the `) -> bool {` line; the head must be // that line, NOT `fn foo(` — the wrapped signature stays visible. let reg = SyntaxRegistry::new(); let src = "fn foo(\n a: u32,\n) -> bool {\n true\n}\n"; let bundle = parse(®, "rust", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "true")).expect("foldable"); assert_eq!(r.start, line_content_end_of(src, ") -> bool {")); assert_eq!(line_text_at(src, r.start), ") -> bool {"); // The two wrapped signature lines are before the fold → visible. assert!(r.start > line_content_end_of(src, "a: u32,")); } #[test] fn head_line_python_uses_def_not_a_body_line() { // R2-1: tree-sitter-python's `block` starts on the first statement // line, so the head must ascend to `def foo():`, not `x = 1`. let reg = SyntaxRegistry::new(); let src = "def foo():\n x = 1\n y = 2\n"; let bundle = parse(®, "python", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "x = 1")).expect("foldable"); assert_eq!(r.start, line_content_end_of(src, "def foo():")); assert_eq!(line_text_at(src, r.start), "def foo():"); assert_eq!(line_text_at(src, r.start + 1), " x = 1"); } #[test] fn head_line_python_wrapped_signature_keeps_signature_visible() { let reg = SyntaxRegistry::new(); let src = "def foo(\n a,\n):\n x = 1\n"; let bundle = parse(®, "python", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "x = 1")).expect("foldable"); assert_eq!(r.start, line_content_end_of(src, "):")); assert_eq!(line_text_at(src, r.start), "):"); } // --------------------------------------------------------------------------- // 4. Range semantics — closer-aware tail (R2-5). // --------------------------------------------------------------------------- #[test] fn brace_closer_line_stays_visible() { let reg = SyntaxRegistry::new(); let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n"; let bundle = parse(®, "rust", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "let x")).expect("foldable"); // Last hidden line is the last body line; the `}` line is outside. assert_eq!(r.end, line_content_end_of(src, "let y = 2;")); assert_eq!( line_text_at(src, r.end + 1), "}", "closer line stays visible" ); } #[test] fn shared_closer_line_else_stays_visible() { // R2-5: `} else {` keeps its trailing sibling on screen. let reg = SyntaxRegistry::new(); let src = "fn f() {\n if a {\n one();\n } else {\n two();\n }\n}\n"; let bundle = parse(®, "rust", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "one()")).expect("foldable"); // The consequent block folds; its `} else {` line stays visible. assert_eq!(line_text_at(src, r.end + 1).trim(), "} else {"); } #[test] fn python_hides_through_last_body_line() { let reg = SyntaxRegistry::new(); let src = "def foo():\n x = 1\n y = 2\n"; let bundle = parse(®, "python", src.as_bytes()); let r = fold_target_at(&bundle, byte_of(src, "x = 1")).expect("foldable"); // Delimiter-less: the last body line is hidden (inside the range). assert_eq!(r.end, line_content_end_of(src, "y = 2")); } // --------------------------------------------------------------------------- // 3. close-all folds top-level regions only; 9. nested + state-aware order. // --------------------------------------------------------------------------- #[test] fn close_all_is_top_level_only() { let reg = SyntaxRegistry::new(); let src = "fn a() {\n if c {\n work();\n more();\n }\n}\n\nfn b() {\n x();\n y();\n}\n"; let bundle = parse(®, "rust", src.as_bytes()); let top = top_level_fold_targets(&bundle); assert_eq!( top.len(), 2, "two top-level fns, the nested `if` is not auto-folded" ); // Neither top-level range is the inner `if` block. let inner = fold_target_at(&bundle, byte_of(src, "work()")).expect("inner foldable"); assert!( !top.contains(&inner), "close-all does not fold the nested region" ); } #[test] fn nested_state_aware_ordering() { // 9 / R3-2: close walks innermost→outer, open walks outer→inner, and // toggle cycles close-inner → close-outer → open-all so every command // reaches the outer fold. let reg = SyntaxRegistry::new(); let src = "fn outer() {\n if cond {\n work();\n more();\n }\n}\n"; let bundle = parse(®, "rust", src.as_bytes()); let p = byte_of(src, "work()"); let mut store = FoldStore::new(); let inner = close_at(&mut store, &bundle, p).expect("close inner"); let outer = close_at(&mut store, &bundle, p).expect("close outer"); assert!( inner.start > outer.start, "inner fold is more deeply nested" ); assert!( close_at(&mut store, &bundle, p).is_none(), "nothing left to close" ); assert_eq!(store.folds().len(), 2); assert_eq!(open_at(&mut store, p), Some(outer), "open outermost first"); assert_eq!(open_at(&mut store, p), Some(inner), "then the inner"); assert!(store.is_empty()); assert!(matches!( cycle_at(&mut store, &bundle, p), CycleOutcome::Closed(_) )); assert!(matches!( cycle_at(&mut store, &bundle, p), CycleOutcome::Closed(_) )); assert_eq!(store.folds().len(), 2, "cycle closed both"); assert!(matches!( cycle_at(&mut store, &bundle, p), CycleOutcome::OpenedAll(2) )); assert!(store.is_empty(), "one more cycle opened them all"); } // --------------------------------------------------------------------------- // 10. Injected layer (a fenced rust block inside markdown). // --------------------------------------------------------------------------- #[test] fn fold_sourced_inside_injected_layer() { let reg = SyntaxRegistry::new(); let src = "# Title\n\n```rust\nfn demo() {\n let x = 1;\n let y = 2;\n}\n```\n\nText.\n"; let bundle = parse(®, "markdown", src.as_bytes()); assert!( bundle.layers.len() >= 2, "markdown fence produced an injected rust layer" ); let r = fold_target_at(&bundle, byte_of(src, "let x")).expect("foldable inside the fence"); assert_eq!( line_text_at(src, r.start), "fn demo() {", "resolved the inner block" ); } // --------------------------------------------------------------------------- // 2. Stale / absent parse tree refuses (the precondition the binding keys on). // --------------------------------------------------------------------------- #[test] fn absent_tree_has_no_current_bundle() { let reg = SyntaxRegistry::new(); let language = reg.language("rust").expect("grammar"); let src = b"fn foo() {\n let x = 1;\n}\n"; let buf = Buffer::from_bytes(BufferId::next(), "doc", src); let view = ParseView::new(&buf, language, "rust".to_owned()); let handle = view.handle(); // Before any parse installs, `current()` is None → the binding refuses. assert!(handle.current().is_none()); let mut req = handle.make_request(); req.injection_aliases = reg.injection_alias_snapshot(); let bundle = run_parse(req).expect("parse"); handle.install(reg.resolve_layer_queries(&bundle)); assert!( handle.current().is_some(), "after install, a target is derivable" ); } // --------------------------------------------------------------------------- // 6. Command-path pre-edit unfold (Q#FD5). // --------------------------------------------------------------------------- fn active_id(s: &EditorState) -> BufferId { s.core.borrow().active_buffer_id() } fn insert_into(s: &EditorState, id: BufferId, text: &str) { let core = s.core.borrow(); let mut reg = core.registry.borrow_mut(); reg.get_mut(id) .unwrap() .apply_edit(EditOp::Insert { pos: 0, bytes: text.as_bytes(), }) .unwrap(); } #[test] fn command_path_self_insert_unfolds_at_point() { let s = EditorState::new_with_roots(&crate::iso::roots()); let id = active_id(&s); insert_into(&s, id, "line0\nline1\nline2\nline3\n"); // Fold the interior of lines 1..2: [end of line0, end of line2]. let store = { let core = s.core.borrow(); let mut reg = core.registry.borrow_mut(); s.fold_registry.store_or_attach(reg.get_mut(id).unwrap()) }; store .lock() .unwrap() .insert(ByteRange { start: 5, end: 17 }); // Cursor strictly inside the fold (start of "line2"). s.core.borrow_mut().set_cursor_byte(12); // A command-path self-insert unfolds before the edit lands. s.core.borrow_mut().insert_char('x'); assert!( store.lock().unwrap().is_empty(), "typing inside a fold unfolds it (Q#FD5)" ); } #[test] fn self_insert_at_head_line_end_does_not_unfold() { // `(start, end]` containment: a self-insert exactly at the end of the // head line (== range.start) is outside the fold — it must NOT unfold, // and the translator shifts the fold right so the char lands visible. let s = EditorState::new_with_roots(&crate::iso::roots()); let id = active_id(&s); insert_into(&s, id, "line0\nline1\nline2\nline3\n"); let store = { let core = s.core.borrow(); let mut reg = core.registry.borrow_mut(); s.fold_registry.store_or_attach(reg.get_mut(id).unwrap()) }; store .lock() .unwrap() .insert(ByteRange { start: 5, end: 17 }); s.core.borrow_mut().set_cursor_byte(5); // end of head line "line0" s.core.borrow_mut().insert_char('x'); let folds = store.lock().unwrap().folds(); assert_eq!( folds, vec![ByteRange { start: 6, end: 18 }], "fold shifts right; the character lands on the head line" ); } // --------------------------------------------------------------------------- // 5. Point moves to the head line when a fold is created around it (Q#FD3). // 11. Data-API validation (Q#FD11) — driven through the Lua surface. // --------------------------------------------------------------------------- fn exec(s: &EditorState, src: &str) { s.lua_host.lua().load(src.to_string()).exec().unwrap(); } fn eval(s: &EditorState, src: &str) -> T { s.lua_host.lua().load(src.to_string()).eval().unwrap() } /// Install a settled rust parse over the active scratch buffer so the /// `pmacs.fold` surface can drive it end to end. fn install_rust_parse(s: &EditorState, id: BufferId) { let reg = &s.syntax_registry; let language = reg.language("rust").expect("grammar"); let handle = { let core = s.core.borrow(); let mut breg = core.registry.borrow_mut(); let buf = breg.get_mut(id).unwrap(); let view = ParseView::new(buf, language, "rust".to_owned()); let handle = view.handle(); buf.attach_view(Box::new(view)); handle }; let mut req = handle.make_request(); req.injection_aliases = reg.injection_alias_snapshot(); let bundle = run_parse(req).expect("parse"); handle.install(reg.resolve_layer_queries(&bundle)); reg.attach_view(id, handle); } #[test] fn folding_moves_point_to_head_line() { let s = EditorState::new_with_roots(&crate::iso::roots()); let id = active_id(&s); let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n"; insert_into(&s, id, src); install_rust_parse(&s, id); // Cursor inside the body; `let x` starts on line 1. let p = byte_of(src, "let x"); s.core.borrow_mut().set_cursor_byte(p); exec(&s, "pmacs.command.invoke('fold.close')"); let head = line_content_end_of(src, "fn foo() {"); assert_eq!( s.core.borrow().active_window().cursor, head, "the invoking point moved to the head line" ); // And the fold exists. let n: i64 = eval(&s, "return #pmacs.fold.folds(pmacs.window.buffer())"); assert_eq!(n, 1); } #[test] fn data_api_validation() { let s = EditorState::new_with_roots(&crate::iso::roots()); // A plain document buffer with four lines. exec( &s, "b = pmacs.buffer.from_bytes('doc.rs', 'aaa\\nbbb\\nccc\\nddd\\n')", ); // A valid multi-line range folds. let ok: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 11 })"); assert!(ok, "a >=1-hidden-line range is accepted"); let n: i64 = eval(&s, "return #pmacs.fold.folds(b)"); assert_eq!(n, 1); // A sub-one-line range (both endpoints on the same line) is rejected. let same_line: bool = eval(&s, "return pmacs.fold.fold(b, { start = 0, ['end'] = 2 })"); assert!( !same_line, "a range hiding no full line is rejected (Q#FD11)" ); // An out-of-bounds range is rejected. let oob: bool = eval( &s, "return pmacs.fold.fold(b, { start = 0, ['end'] = 99999 })", ); assert!(!oob, "an out-of-bounds range is rejected"); // Q#FD9 via the >=1-hidden-line rule: a fold at (0,0) on an empty // buffer normalizes to zero hidden lines and is rejected. exec(&s, "e = pmacs.buffer.from_bytes('empty.rs', '')"); let empty: bool = eval(&s, "return pmacs.fold.fold(e, { start = 0, ['end'] = 0 })"); assert!( !empty, "a zero-length range is rejected (terminals never fold)" ); // Round-trip: unfold the stored range clears it. let unfolded: bool = eval( &s, "local f = pmacs.fold.folds(b)[1]; return pmacs.fold.unfold(b, f)", ); assert!(unfolded); let n2: i64 = eval(&s, "return #pmacs.fold.folds(b)"); assert_eq!(n2, 0); } // --------------------------------------------------------------------------- // 8. Buffer content replacement drops the store. // --------------------------------------------------------------------------- #[test] fn forget_drops_store_and_detaches_view() { let s = EditorState::new_with_roots(&crate::iso::roots()); let id = active_id(&s); insert_into(&s, id, "line0\nline1\nline2\n"); let store = { let core = s.core.borrow(); let mut reg = core.registry.borrow_mut(); s.fold_registry.store_or_attach(reg.get_mut(id).unwrap()) }; store .lock() .unwrap() .insert(ByteRange { start: 5, end: 11 }); assert!(s.fold_registry.store(id).is_some()); // Content replacement (revert/reload) drops the store. { let core = s.core.borrow(); let mut reg = core.registry.borrow_mut(); s.fold_registry.forget(reg.get_mut(id).unwrap()); } assert!(s.fold_registry.store(id).is_none(), "the store is dropped"); assert!( s.fold_registry.folds(id).is_empty(), "no folds survive the drop" ); } #[test] fn killing_a_buffer_through_the_real_path_purges_its_fold_store() { // Drive the production `pmacs.buffer.remove` route (not `forget_buffer` // directly), so the app-data stash + the `after_buffer_removed` branch // are what get exercised — deleting either would leave this red. The // assertion reads through the DEAD id on purpose: BufferIds are never // reused, so a stale id cannot alias a later buffer. Mirrors // config_registry's `killing_a_buffer_through_the_real_path_...`. let s = EditorState::new_with_roots(&crate::iso::roots()); exec( &s, "b = pmacs.buffer.from_bytes('kill.rs', 'aaa\\nbbb\\nccc\\nddd\\n')", ); let id = { let core = s.core.borrow(); let reg = core.registry.borrow(); reg.find_by_name("kill.rs").expect("buffer") }; // Fold via the data API (no parse tree needed) so the store exists. let folded: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 11 })"); assert!(folded); assert!( s.fold_registry.store(id).is_some(), "store exists before kill" ); exec(&s, "pmacs.buffer.remove(b)"); assert!( s.fold_registry.store(id).is_none(), "the real kill path purges the fold store" ); } #[test] fn close_all_command_moves_point_to_enclosing_head() { // Q#FD3 through the command surface: `fold.close-all` is interactive, // so when it collapses a top-level fold around the invoking point, the // point moves to that fold's head line (Finding 3, round 1). let s = EditorState::new_with_roots(&crate::iso::roots()); let id = active_id(&s); let src = "fn first() {\n a();\n b();\n}\nfn second() {\n c();\n d();\n}\n"; insert_into(&s, id, src); install_rust_parse(&s, id); // Point inside the SECOND function's body. s.core.borrow_mut().set_cursor_byte(byte_of(src, "c()")); exec(&s, "pmacs.command.invoke('fold.close-all')"); let n: i64 = eval(&s, "return #pmacs.fold.folds(pmacs.window.buffer())"); assert_eq!(n, 2, "both top-level functions are folded"); assert_eq!( s.core.borrow().active_window().cursor, line_content_end_of(src, "fn second() {"), "the invoking point moved to the enclosing fold's head line" ); } #[test] fn stale_parse_tree_refuses_fold() { // Q#FD10: an edit after the parse leaves `pending_edit_count() > 0`, // so a fold command refuses (the settled coordinates are stale). let s = EditorState::new_with_roots(&crate::iso::roots()); let id = active_id(&s); let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n"; insert_into(&s, id, src); install_rust_parse(&s, id); // A further edit accumulates a pending edit on the attached parse view. insert_into(&s, id, "// stale\n"); s.core.borrow_mut().set_cursor_byte(20); exec(&s, "pmacs.command.invoke('fold.close')"); let n: i64 = eval(&s, "return #pmacs.fold.folds(pmacs.window.buffer())"); assert_eq!(n, 0, "a stale parse tree refuses fold creation"); } #[test] fn read_only_buffer_is_rejected() { // Q#FD11's "normal document buffer" guard: terminals are read-only, so // a read-only buffer is not foldable. let s = EditorState::new_with_roots(&crate::iso::roots()); exec( &s, "b = pmacs.buffer.from_bytes('ro.rs', 'aaa\\nbbb\\nccc\\n')", ); let id = { let core = s.core.borrow(); let reg = core.registry.borrow(); reg.find_by_name("ro.rs").expect("buffer") }; { let core = s.core.borrow(); let mut reg = core.registry.borrow_mut(); reg.get_mut(id).unwrap().set_read_only(true); } let ok: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 7 })"); assert!(!ok, "a read-only buffer is rejected (Q#FD11)"); } #[test] fn unfold_normalizes_an_arbitrary_range_to_a_stored_fold() { let s = EditorState::new_with_roots(&crate::iso::roots()); exec( &s, "b = pmacs.buffer.from_bytes('doc.rs', 'aaa\\nbbb\\nccc\\nddd\\n')", ); let folded: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 11 })"); assert!(folded); // A *different* input range that normalizes to the same stored fold. let unfolded: bool = eval( &s, "return pmacs.fold.unfold(b, { start = 1, ['end'] = 10 })", ); assert!(unfolded, "unfold normalizes an arbitrary range"); let n: i64 = eval(&s, "return #pmacs.fold.folds(b)"); assert_eq!(n, 0); } // Isolated bootstrap storage roots (see the module docs): an // integration test is compiled without `cfg(test)`, so a raw // `EditorState::new()` would read the developer's real `init.lua` and // write into their real data root. #[path = "common/iso.rs"] mod iso;