Skip to content

ADR 0127: The buffer with nothing to fall back on

Status

Accepted

Context

ADR 0110 gave unsaved work a snapshot that survives a crash. It filed each snapshot under the buffer's file path, and both armRecoverySnapshot() and writeRecoverySnapshot() opened with

if (m_recoveryDir.isEmpty() || m_filePath.isEmpty()) {
    return;
}

So a buffer with no path was never snapshotted at all. Ctrl+N, type for an hour, crash, and everything is gone — the one case where there is no file on disk to fall back to, and the only one the feature did not cover.

The second gap was the other end: nothing ever removed a snapshot for a file crashed on and never reopened, so <config>/recovery/ grew for good.

Decision

An identity that is not a path

A snapshot is filed under a key. For a saved file that is still its path. For a buffer without one it is untitled:<pid>:<serial>.

The pid keeps two running editors apart; the serial keeps two untitled buffers in one editor apart. No absolute path begins untitled:, so nothing can collide with a real file.

saveAs() drops the snapshot before moving to the new key, or the untitled one would be left behind and offered back later as work that was lost.

Offered at startup, because nothing will reopen it

A named file's snapshot is found when you open that file. An untitled one has no such moment, so the window walks the directory at startup and asks.

ase_recovery_list() is that walk. It reads each file's recorded key, so anything in the directory that is not a whole, well-formed snapshot — truncated, or not one at all — is skipped rather than becoming a phantom entry in a prompt.

A key whose pid still belongs to a running process is left alone. The alternative is that opening a second window offers back a buffer that is on screen in the first one.

A clean exit is not a crash

Closing a buffer you were asked about, or quitting past the unsaved-changes prompt, now drops the snapshot. Work you were asked about and chose to drop is not work the editor lost, and leaving it would offer it back at the next start having just been told to discard it.

Pruning

ase_recovery_prune() removes snapshots untouched for 30 days, run at startup. A snapshot from the future is not an old one, so a clock that moved backwards deletes nothing.

Consequences

Recovering an untitled buffer opens it as unsaved — it has content and nothing on disk, so the dot is the truth. The undo stack cannot say so, because the text goes in before there is one, so it is marked through the same flag Ctrl+W-mid-insert already used.

A bare launch leaves one empty untitled buffer; recovering into a new tab beside it left that as clutter, so it is closed, but only while it is genuinely untouched.

Verified against a real SIGKILL rather than a simulated one: type into an untitled buffer, kill -9, restart, and the prompt appears and the text comes back. The same run confirms a clean :q! leaves nothing, that Discard removes it, and that a second instance is not offered the first one's live buffer.

Three tests in gui/tests/test_recovery_flow.cpp cover the viewport half; they fail on the old code with hasRecoverySnapshot() returning false. Four in core/tests/test_recovery.c cover listing and pruning.

Writing them turned up that the suite's cleanup only rmdir'd, which fails silently on a non-empty directory — so a failed run left debris that broke a different test next time, which is exactly what happened once here. Cleanup now removes the files, and the test that needed "the only snapshot in the directory" asks for its own by key instead.