The watcher that cried delete
Margin watches your folder so external edits reflow into the app. The hard part isn't noticing changes. It's not over-reacting to them.
Margin is a view over a folder of Markdown files, so it watches that folder. Edit a note in vim, pull a branch, drop a file in from Finder, and the app should reflow without being told.
The hard part isn't noticing that something changed. It's not over-reacting to it.
The event that isn't true
A lot of editors don't save by writing into the existing file. They write a temporary file and rename it over the top, so a reader never sees a half-written document. Vim does it. So does Margin.
The side effect is that a watcher sees a deletion followed by a creation, milliseconds apart. If you trust the event kind, your app confidently announces that the note the user is looking at has been deleted, while the replacement is already landing on disk.
The file never went anywhere. Only the inode did.
You can chase that with a re-check: when a delete arrives, stat the path again before you believe it. That works, and it's what I'd reach for first too. But it leaves you deciding how long to wait, and a genuine deletion now has to lose a race on purpose.
Not reading the event kind at all
What I do instead is refuse the question. The watcher filters to .md paths, debounces for 300ms, and emits a single event that carries no detail beyond which space changed. The front end re-reads the folder.
let Ok(first) = rx.recv() else { break };
let mut paths = first;
let deadline = Instant::now() + DEBOUNCE;
while let Ok(more) = rx.recv_timeout(deadline.saturating_duration_since(Instant::now())) {
paths.extend(more);
}
A delete and a create two milliseconds apart collapse into one event, and by the time anything re-reads, the replacement is there. It costs a directory listing I don't strictly need, and in exchange I never have to decide whether a Remove was real.
There's a second suppression on top: paths the app wrote itself within the last second and a half are ignored, so Margin's own saves don't echo back through the watcher as external changes.
Identity can't be the path
The other half of the problem is what an open note is.
If a note is identified by its file path, then an atomic save has already broken it, because the path briefly pointed at nothing. Rename the file in Finder and it breaks again, permanently.
So every note carries an id in its frontmatter. The file is where the note currently lives, not what it is. A replace, a rename, a move into another folder: none of them orphan whatever is open on screen, because identity travels inside the file rather than alongside it.
That decision came out of the watcher work, but it paid for itself somewhere else entirely. Links between notes point at ids, so reorganising a folder at eleven at night doesn't break a single one.
The general shape of the lesson, which I suspect applies well beyond file watching: an event tells you that something happened, not what is now true. When those two are cheap to separate, separate them. Re-read the world instead of reconstructing it from the notifications.