Files
Marcos Paulo 15d7f5e873 feat: editor blocks and lint, vault knowledge, calendar plugin, macOS menu
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity
- Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys
- Visual view: tables, note embeds, %% comments, wrapping property chips
- Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin
- Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 12:36:00 -03:00

86 lines
2.9 KiB
Rust

//! Fix application: idempotence, staleness, overlap.
use notescraft_lint::{Context, Edit, Fix, Range, check, fix};
const MESSY: &str = "---\ntitle: A\n\ntext \n# A\n### B \n* x y\n- z\nafter\n```\ncode\n```\nend\n";
#[test]
fn safe_fixes_are_idempotent() {
let ctx = Context::default();
let mut text = MESSY.to_string();
for _ in 0..4 {
let issues = check(&text, &ctx);
let (next, ..) = fix::apply_all_safe(&text, &issues);
text = next;
}
let left = check(&text, &ctx);
none(&left);
let (same, applied, skipped) = fix::apply_all_safe(&text, &left);
assert_eq!((same.as_str(), applied, skipped), (text.as_str(), 0, 0));
}
#[test]
fn single_fix_removes_its_issue() {
let text = "# A\n\n### B\n";
let issues = check(text, &Context::default());
let out = fix::apply(text, issues[0].fix.as_ref().expect("fix")).expect("apply");
assert_eq!(out, "# A\n\n## B\n");
none(&check(&out, &Context::default()));
}
fn edit(start: usize, end: usize, text: &str) -> Edit {
Edit { range: Range { start, end }, text: text.into() }
}
fn fix_of(edits: Vec<Edit>) -> Fix {
Fix { label: "x".into(), edits, safe: true }
}
#[test]
fn stale_fixes_are_refused() {
let text = "héllo";
for bad in [edit(0, 99, ""), edit(2, 3, ""), edit(3, 1, ""), edit(99, 99, "x")] {
assert_eq!(fix::apply(text, &fix_of(vec![bad])), Err(notescraft_lint::Error::Stale));
}
let overlap = fix_of(vec![edit(0, 3, ""), edit(2, 4, "")]);
assert_eq!(fix::apply("abcdef", &overlap), Err(notescraft_lint::Error::Stale));
let same_point = fix_of(vec![edit(1, 1, "a"), edit(1, 1, "b")]);
assert!(fix::apply("abc", &same_point).is_err());
assert_eq!(fix::apply("abc", &fix_of(vec![edit(2, 3, "Z"), edit(0, 1, "Y")])).as_deref(), Ok("YbZ"));
}
#[test]
fn overlapping_safe_fixes_are_skipped_not_merged() {
let text = "a b\n";
let mut issues = check(text, &Context::default());
let mut dup = issues[0].clone();
dup.fix = Some(fix_of(vec![edit(1, 2, "-")]));
issues.push(dup);
let (out, applied, skipped) = fix::apply_all_safe(text, &issues);
assert_eq!((applied, skipped), (1, 1));
assert!(out == "a b\n" || out == "a- b\n");
}
#[test]
fn link_fixes_are_never_applied_in_bulk() {
let ctx = Context { titles: vec![("Home.md".into(), "Home".into())], ..Context::default() };
let text = "[[Hom]]\n";
let issues = check(text, &ctx);
assert_eq!(issues.len(), 1);
let (out, applied, skipped) = fix::apply_all_safe(text, &issues);
assert_eq!((out.as_str(), applied, skipped), (text, 0, 0));
}
#[test]
fn closing_frontmatter() {
let text = "---\na: 1\nb: 2";
let issues = check(text, &Context::default());
let out = fix::apply(text, issues[0].fix.as_ref().expect("fix")).expect("apply");
assert_eq!(out, "---\na: 1\nb: 2\n---\n");
none(&check(&out, &Context::default()));
}
fn none<T: std::fmt::Debug>(v: &[T]) {
assert_eq!(format!("{v:?}"), "[]");
}