diff --git a/docs/active-work.md b/docs/active-work.md index de60c4f..2d364c6 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -61,18 +61,21 @@ If it does not, stop and repair the remote/fetch configuration. - Framing head: revision 5 of `docs/folding-framing.md` (rev 1 → … → rev 4 absorbed three review rounds; rev 5 records approval + the Q#FD4 binding decision). -- State: **APPROVED; Stage 1 (fold engine, headless) implementing on this - branch.** Bindings decided (Q#FD4 → Emacs hideshow `C-c @` set); Bet B1 - accepted as framed. +- State: **Stage 1 (fold engine, headless) implemented; PR #142 OPEN**, + two review rounds landed on the branch. Bindings decided (Q#FD4 → Emacs + hideshow `C-c @` set); Bet B1 accepted as framed. Load-bearing decision (Q#FD1): the bundled grammars ship no fold query and no `folds.scm`, so the roadmap's "tree-sitter fold ranges" is not free; v1 is structural node folding (block-like node ≥2 source lines, derived head line, closer-aware tail), with indentation fallback and curated queries - deferred. `FoldState` already exists in the protocol, declared-but-unproduced - (a test pins it is never emitted); no frontend consumes it yet; gutter - markers are frontend-derived like the diagnostic sign bars, so no new wire - type. Staged like vterm: Stage 1 engine (headless), Stage 2 TUI, Stage 3 GPU. -- PR: Stage 1 opens as the first folding PR once the gate suite is green. + deferred. `FoldState` already exists in the protocol; Stage 1 starts + *producing* it (authoritative-empty), no protocol bump; gutter markers are + frontend-derived like the diagnostic sign bars, so no new wire type. Staged + like vterm: Stage 1 engine (headless), Stage 2 TUI, Stage 3 GPU. +- PR: **#142** (`Arc 6 folding — Stage 1: instance fold engine`), open + against `main`. Round 1 (tail-boundary delete bug + buffer-kill cleanup + + close-all point-move) and round 2 (pin the kill-path + close-all through + the real command surface) are landed as fix commits on the branch. - Next: land Stage 1; Stages 2/3 are separate branches/PRs, each re-framed in detail after the prior stage lands. diff --git a/tests/folding_acceptance.rs b/tests/folding_acceptance.rs index 3f9977e..6df069a 100644 --- a/tests/folding_acceptance.rs +++ b/tests/folding_acceptance.rs @@ -493,24 +493,60 @@ fn forget_drops_store_and_detaches_view() { } #[test] -fn forget_buffer_drops_store_on_kill() { - // The id-keyed reset the pmacs.buffer.kill path uses: the buffer (and - // its attached view) is gone, so only the map entry is cleared. +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(); + 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(); let id = active_id(&s); - insert_into(&s, id, "line0\nline1\nline2\n"); - { - let core = s.core.borrow(); - let mut reg = core.registry.borrow_mut(); - s.fold_registry - .store_or_attach(reg.get_mut(id).unwrap()) - .lock() - .unwrap() - .insert(ByteRange { start: 5, end: 11 }); - } - assert!(s.fold_registry.store(id).is_some()); - s.fold_registry.forget_buffer(id); - assert!(s.fold_registry.store(id).is_none()); + 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]