//! Acceptance: a JSON format or minify rewrite is ONE undo step. //! //! `json.format_block` and `json.minify_block` go through `Session::lint_commit`, which edits //! the open buffer as a single replacement: one `buffer.undo` takes the whole format back, //! `buffer.redo` re-applies it, and unsaved edits elsewhere in the note survive. Without an //! open buffer the same edit is one atomic write, and formatting an already formatted block //! reports `changed: false` and leaves the file untouched. For comparison, `block.turn_into` //! and `block.move` are checked to be one undo step as well. mod common; use common::{TempVault, on_disk, seed, temp_vault}; use notescraft_engine::{Registry, Session, register_core}; use serde_json::{Value, json}; /// The fixture note: one fenced JSON block with compact JSON. const NOTE: &str = "# T\n\n```json\n{\"b\":1,\"a\":[1e5]}\n```\n"; /// The same note with the JSON pretty-printed; the ONLY change a format makes. const FORMATTED: &str = "# T\n\n```json\n{\n \"b\": 1,\n \"a\": [\n 1e5\n ]\n}\n```\n"; /// A vault with `n.md` holding `note`, a registry with the core commands, and an open session. fn setup(name: &str, note: &str) -> (Registry, Session, TempVault) { let vault = temp_vault(name); seed(vault.path(), &[("n.md", note)]); let mut registry = Registry::new(); register_core(&mut registry).expect("register"); let session = Session::open(vault.path()).expect("open"); (registry, session, vault) } /// Runs `id` on the session, panicking with the error instead of returning a bare `Err`. fn run(registry: &Registry, session: &mut Session, id: &str, params: &Value) -> Value { registry.run(id, session, params).unwrap_or_else(|error| panic!("{id}: {error:?}")) } /// The current text of the `n.md` buffer. fn text(session: &Session) -> String { session.buffer("n.md").expect("the buffer is open").text().to_owned() } /// One `json.format_block` is one undo step: undo restores the exact original, redo re-applies /// it, and a later `json.minify_block` is one undo step too. #[test] fn json_format_and_minify_are_one_undo_step_each() { let (registry, mut session, vault) = setup("json-undo", NOTE); run(®istry, &mut session, "buffer.open", &json!({ "path": "n.md" })); let result = run(®istry, &mut session, "json.format_block", &json!({ "path": "n.md", "index": 1 })); assert_eq!(result["changed"], json!(true)); assert_eq!(text(&session), FORMATTED); assert_eq!(on_disk(vault.path(), "n.md"), NOTE, "a buffer edit is not a save"); let result = run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(result["undone"], json!(true)); assert_eq!(text(&session), NOTE, "ONE undo restores the exact original text"); assert_eq!(result["dirty"], json!(false), "the buffer is back to the saved bytes"); let result = run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(result["undone"], json!(false), "the format was ONE step: nothing more to undo"); let result = run(®istry, &mut session, "buffer.redo", &json!({ "path": "n.md" })); assert_eq!(result["redone"], json!(true)); assert_eq!(text(&session), FORMATTED, "ONE redo re-applies the format"); let result = run(®istry, &mut session, "json.minify_block", &json!({ "path": "n.md", "index": 1 })); assert_eq!(result["changed"], json!(true)); assert_eq!(text(&session), NOTE, "the minified body is the original compact body"); run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(text(&session), FORMATTED, "the minify is ONE undo step, not more"); } /// For comparison: `block.turn_into` and `block.move` are one undo step each in an open buffer. #[test] fn turn_into_and_move_are_one_undo_step_each() { const PROSE: &str = "# Title\n\nFirst para\n\nSecond para\n"; let (registry, mut session, _vault) = setup("block-undo-turn", PROSE); run(®istry, &mut session, "buffer.open", &json!({ "path": "n.md" })); run(®istry, &mut session, "block.turn_into", &json!({ "path": "n.md", "index": 1, "target": "heading2" })); assert_eq!(text(&session), "# Title\n\n## First para\n\nSecond para\n"); run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(text(&session), PROSE, "ONE undo restores the original"); let result = run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(result["undone"], json!(false), "the turn into was ONE step"); let (registry, mut session, _vault) = setup("block-undo-move", PROSE); run(®istry, &mut session, "buffer.open", &json!({ "path": "n.md" })); run(®istry, &mut session, "block.move", &json!({ "path": "n.md", "index": 2, "direction": "up" })); assert_eq!(text(&session), "# Title\n\nSecond para\n\nFirst para\n"); run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(text(&session), PROSE, "ONE undo restores the original"); } /// A buffer with unsaved user edits elsewhere keeps them: the format touches only the JSON /// block, and one undo removes only the format. #[test] fn format_keeps_unsaved_edits_elsewhere_in_the_buffer() { const ORIGINAL: &str = "# T\n\nIntro\n\n```json\n{\"b\":1,\"a\":[1e5]}\n```\n"; const EDITED: &str = "# T\n\nOutro\n\n```json\n{\"b\":1,\"a\":[1e5]}\n```\n"; let (registry, mut session, vault) = setup("json-undo-dirty", ORIGINAL); run(®istry, &mut session, "buffer.open", &json!({ "path": "n.md" })); let start = text(&session).find("Intro").unwrap_or_default(); let end = start.saturating_add("Intro".len()); session.buffer_mut("n.md").expect("the buffer is open").edit(start..end, "Outro").expect("user edit"); assert_eq!(text(&session), EDITED); let result = run(®istry, &mut session, "json.format_block", &json!({ "path": "n.md", "index": 2 })); assert_eq!(result["changed"], json!(true)); assert_eq!(text(&session), "# T\n\nOutro\n\n```json\n{\n \"b\": 1,\n \"a\": [\n 1e5\n ]\n}\n```\n"); assert_eq!(on_disk(vault.path(), "n.md"), ORIGINAL, "nothing is saved behind the user's back"); run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(text(&session), EDITED, "undo removes only the format, the user edit stays"); run(®istry, &mut session, "buffer.redo", &json!({ "path": "n.md" })); assert_eq!(text(&session), "# T\n\nOutro\n\n```json\n{\n \"b\": 1,\n \"a\": [\n 1e5\n ]\n}\n```\n"); } /// Without an open buffer the format is one write: an already formatted block reports /// `changed: false` and leaves the file bytes and mtime untouched. #[test] fn format_without_an_open_buffer_writes_the_file_once() { let (registry, mut session, vault) = setup("json-no-buffer", NOTE); let result = run(®istry, &mut session, "json.format_block", &json!({ "path": "n.md", "index": 1 })); assert_eq!(result["changed"], json!(true)); assert_eq!(on_disk(vault.path(), "n.md"), FORMATTED, "the format reached the disk"); let mtime = std::fs::metadata(vault.path().join("n.md")).and_then(|meta| meta.modified()).expect("mtime"); let result = run(®istry, &mut session, "json.format_block", &json!({ "path": "n.md", "index": 1 })); assert_eq!(result["changed"], json!(false), "the block is already formatted"); assert_eq!(on_disk(vault.path(), "n.md"), FORMATTED, "the bytes are identical"); let again = std::fs::metadata(vault.path().join("n.md")).and_then(|meta| meta.modified()).expect("mtime"); assert_eq!(again, mtime, "an unchanged format does not rewrite the file"); } /// A CRLF note keeps CRLF everywhere, also inside the formatted JSON (no mixed line endings). #[test] fn format_in_a_crlf_note_keeps_crlf_outside_the_json() { const CRLF: &str = "# T\r\n\r\n```json\r\n{\"b\":1,\"a\":[1e5]}\r\n```\r\n"; let (registry, mut session, _vault) = setup("json-crlf", CRLF); run(®istry, &mut session, "buffer.open", &json!({ "path": "n.md" })); let result = run(®istry, &mut session, "json.format_block", &json!({ "path": "n.md", "index": 1 })); assert_eq!(result["changed"], json!(true)); let after = text(&session); assert_eq!(after, "# T\r\n\r\n```json\r\n{\r\n \"b\": 1,\r\n \"a\": [\r\n 1e5\r\n ]\r\n}\r\n```\r\n"); assert!(after.starts_with("# T\r\n\r\n```json\r\n"), "the lines outside the JSON keep CRLF"); run(®istry, &mut session, "buffer.undo", &json!({ "path": "n.md" })); assert_eq!(text(&session), CRLF, "ONE undo restores the CRLF original"); }