Skip to content

fix(save): a buffer with no file has no baseline to have moved - #723

Merged
PathGao merged 1 commit into
masterfrom
fix/untitled-first-save
Aug 27, 2026
Merged

fix(save): a buffer with no file has no baseline to have moved#723
PathGao merged 1 commit into
masterfrom
fix/untitled-first-save

Conversation

@PathGao

@PathGao PathGao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What this is

In 2.7.5, a new document cannot be saved. Ctrl+S on an untitled buffer opens the Save dialog, the user names the file, and nothing is written: the buffer stays dirty, the conflict bar goes up, and closing the tab asks about unsaved changes. Renaming and trying again is refused the same way, so the document has no exit but the clipboard. Reported by @PathGao on macOS 2.7.5; the path is platform-independent.

Files already on disk are unaffected — their baseline matches, so their saves go through as before.

Mechanism

fileDiffersFromBaseline (#698) compares the file on disk against tab.originalContent before every overwrite, and treats an unreadable file as changed:

Ctrl+S on an untitled tab
  └─ Save dialog → targetPath = /notes/Untitled 1.md
       └─ fileDiffersFromBaseline(tab, targetPath)
            └─ read_file_content_checked → Err (nothing there yet)
                 └─ catch → true  ("changed")
                      └─ onDiskChangedUnderSave → save returns false

"Unreadable means changed" is right for a file this tab has written before — mid-write or gone are both reasons to stop and ask. It is not a question that can be asked about a path the dialog produced one line earlier, which names no file by construction. And the bar it raises is unanswerable here: resolveExternalChangeByReloading returns on !tab.path, so Reload does nothing, and only "keep mine" (allowOverwriteOnce) lets the next save through.

What changed

fileDiffersFromBaseline answers false for a tab with no path. An untitled buffer is a version OF nothing, so there is no baseline for a third party to have moved.

Considered instead: skipping the call in saveContent when the dialog produced the path. Same condition (saveContent opens the dialog exactly when !tab.path), but it leaves the wrong answer inside the predicate for the next caller to find, and short-circuiting past overwriteAllowed.delete would strand an authorization. Also considered treating "file does not exist" as unchanged in the catch: that widens the hole for a tab whose file was deleted underneath it, which is a case the guard should keep refusing.

Saving an untitled buffer onto a file that does exist is the one case that looks like an overwrite. The Save dialog has already asked "replace?" and the user answered it; this check would be a second, worse copy of that question, asked against a baseline this tab never had. That is what the second test pins.

Scope

Only the untitled case. The guard still refuses an overwrite when a tab's own file moved under it (third test), and Save As is untouched — it never had this check, which is why it kept working and is the workaround for anyone on 2.7.5.

Tests

scripts/untitledFirstSaveWrites.spec.ts, 3 tests. Reverting the fix and keeping the tests turns 2 of the 3 red (the first save, and the save onto an existing file); the third — a real tab still refusing a changed file — is green either way by design, since it is the behavior being preserved.

The stub is why this shipped. Existing specs answer a read of an unknown path with '' instead of rejecting, so under test the missing file looked like an empty one and the guard compared two empty strings. This file's stub rejects the way read_file_content_checked does.

Verification

npm audit clean, npm run check 817 files / 0 errors, npm test 987 pass, cargo test unchanged (no Rust touched). npm run test:vitest adds 3 passing and no new failures; that suite already has 14 failures on a clean origin/master (tab navigation history, window tags, settings persistence, undo history) which this branch neither causes nor fixes.

Not verified by hand. A macOS release build of this branch exists and nobody has driven it yet; the manual check to run is new document, type, Ctrl+S, no rename, and see the file appear with the tab clean. Nothing in the path is platform-specific, and Windows and Linux were not tried either.

The external-change guard asks the disk "has this file changed since I
last saw it?" before every overwrite, and answers "changed" when the read
fails — the safe answer for a file the tab has written before, and the
wrong question entirely for a path the Save dialog has only just named.
Nothing is there to read yet, so the first save of every new document was
refused: no file written, the buffer still dirty, and the conflict bar up
on a tab whose Reload has no file to reload. Retrying under another name
went down the same path and was refused the same way, so a new document
could not be saved at all.

An untitled buffer is a version OF nothing: there is no baseline for a
third party to have moved, and nothing for the guard to protect. Saving
onto a file that does exist is the one case that looks like an overwrite,
and the Save dialog has already asked about replacing it — this check
would be a worse second copy of that question, asked against a baseline
this tab never had.
@PathGao

PathGao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@alecdotdev
I merge this and you ship 2.7.6!really sorry for break the app. I should use beta for few days(I did but not find the big issue)

@PathGao
PathGao merged commit 0b74565 into master Aug 27, 2026
4 checks passed
@PathGao
PathGao deleted the fix/untitled-first-save branch August 27, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant