diff --git a/src/store.rs b/src/store.rs index e4d58f6..743ba7a 100644 --- a/src/store.rs +++ b/src/store.rs @@ -194,6 +194,29 @@ mod tests { assert!(s.get("Other/d.md").is_some()); } + #[test] + fn note_delete_and_prefix_delete_of_one_path_are_disjoint() { + // A vanished `*.md` path sends both, since it may have been a + // directory [ANW-40]. The note delete takes the key `Archive.md`, + // the prefix delete takes the keys under `Archive.md/`; neither + // reaches anything else. + let s = NoteStore::new(); + s.replace(vec![ + note("Archive.md"), + note("Archive.md/inner.md"), + note("Keep.md"), + ]); + let dropped = s.apply_batch( + vec![], + &["Archive.md".to_string()], + &["Archive.md".to_string()], + ); + assert_eq!(dropped, 1); + assert!(s.get("Archive.md").is_none()); + assert!(s.get("Archive.md/inner.md").is_none()); + assert!(s.get("Keep.md").is_some()); + } + #[test] fn apply_batch_prefix_delete_precedes_upserts() { let s = NoteStore::new(); diff --git a/src/watcher.rs b/src/watcher.rs index 55c0620..f004db0 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -124,13 +124,22 @@ fn appear(vault_root: &Path, abs: &Path) -> Option { /// A path that is gone: a note to drop, or a directory whose notes are all /// gone with it. The filesystem cannot be asked -- it no longer holds the -/// entry -- so the decision rests on the path's own shape. -fn vanish(vault_root: &Path, abs: &Path) -> Option { - if is_markdown(abs) { - note_relative(vault_root, abs).map(WatchAction::Delete) - } else { - tree_relative(vault_root, abs).map(WatchAction::DeleteTree) +/// entry -- so both are emitted and the index decides which one matches. +/// +/// A `.md` suffix does not prove the path was a file: a directory may carry +/// it too, and then the suffix test alone strands its notes in the index +/// [ANW-40]. The two key sets are disjoint -- the note delete removes the key +/// `dir`, the prefix delete removes keys under `dir/` -- so emitting both is +/// always safe. It costs one range scan per deleted note. +fn vanish(vault_root: &Path, abs: &Path) -> Vec { + let mut actions = Vec::new(); + if let Some(rel) = note_relative(vault_root, abs) { + actions.push(WatchAction::Delete(rel)); } + if let Some(rel) = tree_relative(vault_root, abs) { + actions.push(WatchAction::DeleteTree(rel)); + } + actions } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -454,7 +463,10 @@ mod tests { ); assert_eq!( map_event(&e, &vault()), - vec![WatchAction::Delete("a.md".into())] + vec![ + WatchAction::Delete("a.md".into()), + WatchAction::DeleteTree("a.md".into()) + ] ); } @@ -480,6 +492,7 @@ mod tests { map_event(&e, &vault()), vec![ WatchAction::Delete("a.md".into()), + WatchAction::DeleteTree("a.md".into()), WatchAction::Upsert("b.md".into()) ] ); @@ -490,7 +503,25 @@ mod tests { let e = ev(EventKind::Remove(RemoveKind::File), &["/v/a.md"]); assert_eq!( map_event(&e, &vault()), - vec![WatchAction::Delete("a.md".into())] + vec![ + WatchAction::Delete("a.md".into()), + WatchAction::DeleteTree("a.md".into()) + ] + ); + } + + #[test] + fn removed_md_directory_drops_the_notes_under_it() { + // A directory may be named `*.md` too. The vanished path cannot be + // probed, so the note delete and the prefix delete both go out and + // the index applies whichever matches [ANW-40]. + let e = ev(EventKind::Remove(RemoveKind::Folder), &["/v/Archive.md"]); + assert_eq!( + map_event(&e, &vault()), + vec![ + WatchAction::Delete("Archive.md".into()), + WatchAction::DeleteTree("Archive.md".into()) + ] ); } @@ -588,7 +619,10 @@ mod tests { let absent = ev(kind, &[]).add_path(root.path().join("gone.md")); assert_eq!( map_event(&absent, root.path()), - vec![WatchAction::Delete("gone.md".into())] + vec![ + WatchAction::Delete("gone.md".into()), + WatchAction::DeleteTree("gone.md".into()) + ] ); }