15d7f5e873
- 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>
86 lines
2.9 KiB
Rust
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:?}"), "[]");
|
|
}
|