Conversation
…ed file - Await the format command so the document write captures the formatted content instead of racing an async formatter - Only format when the file being saved is the active file: formatters operate on the active editor document, so a tab switch mid-save could previously rewrite the wrong file - Toggle markChanged on the file being saved, not whatever tab is active - Abort the save when the formatter fails instead of writing unformatted content and reporting success - acode.format() now returns null (not a failure) when no formatter is configured, so format-on-save keeps working for unformatted file types; the format command propagates the tri-state result Fixes Acode-Foundation#2900
|
| file.markChanged = false; | ||
| try { | ||
| acode.exec("format", false); | ||
| if (editorManager.activeFile === file) { |
There was a problem hiding this comment.
The active-file check only runs before awaiting the formatter. Prettier reads the original document, awaits configuration and formatting, and then dispatches through the shared editor instance. If the user switches tabs during those awaits, the editor state points to the newly active document, so the old file's formatted text is applied to the wrong tab. Revalidate the active file and document before applying formatter edits, or otherwise keep the formatter tied to its original target.
Knowledge Base Used:
| const formatted = await acode.exec("format", false); | ||
| if (formatted === false) return false; |
There was a problem hiding this comment.
Formatter failures become success
This check cannot detect the built-in formatters' normal failure results. acode.format() discards the value returned by formatter.format() and returns true whenever the callback resolves, while both Prettier and LSP resolve false for handled failures. Those failures therefore still write the unformatted document and report a successful save. The formatter's boolean result needs to propagate through acode.format().
Knowledge Base Used: Shared application services
| // `null` means no formatter is configured, which is not | ||
| // a failure. | ||
| const formatted = await acode.exec("format", false); | ||
| if (formatted === false) return false; |
There was a problem hiding this comment.
On the Save As and new-file paths, formatting runs only after the target has been created, the session URI has been reassigned, any open session for that URI has been displaced, and recents have been updated. If formatting then fails, this return aborts the content write without rolling back those changes. The result is an empty target and an open tab pointing at a file that was never successfully saved. Format before these mutations or restore all affected state when formatting aborts.
Knowledge Base Used: Workspace file sessions
… masking - Propagate formatter.format()'s boolean through acode.format() (true = ran, false = failed, null = no formatter) so a handled formatter failure aborts the save instead of writing unformatted content as a successful save. - Revalidate the active file after the formatter resolves in acode.format(); a mid-format tab switch now reports failure. - Abort the format dispatch itself when the tab switched mid-format: prettierFormatter and LSP formatDocument return false without writing instead of dispatching into the newly active document. - Run formatting before Save As/new-file target mutations (file creation, URI reassignment, recents updates) so a failed format aborts before any of them happen.
Problem
Fixes #2900.
Format on Save had three races/corruption vectors in
src/lib/saveFile.js:editorManager.activeFile.markChangedand ran the formatter against the active tab. If the user switched from file A to file B while an async save step (e.g. a slow providerexists()check during Save As) was pending, the formatter rewrote B while the save still wrote A.acode.exec("format", false)was fire-and-forget, so an async formatter could finish after the source write, leaving the saved content unformatted.Fix
saveFile.js: only run format-on-save when the file being saved is the active file (formatters operate on the active editor document by design — see below), togglemarkChangedon the file being saved rather than whatever tab happens to be active,awaitthe format so the write captures the formatted document, and abort the save when the formatter fails.commands.js: theformatcommand now returns the format result so callers can react to it.acode.js:acode.format()returnsnull(instead offalse) when no formatter is configured for the mode. Both are falsy for existing callers, but it lets save distinguish "nothing to do" (save proceeds) from "formatter failed" (save aborts) — so enabling format-on-save globally no longer breaks saving file types without a formatter.Known limitation (documented, not hidden)
The built-in formatters (Prettier, LSP) are view-bound: they read and rewrite the shared editor document, so they can only format the active tab. Truly formatting a background tab needs document-based formatting — a bigger architectural change. This PR makes the current architecture safe (no cross-file corruption, no phantom writes) rather than pretending to fix what it can't.
Testing
biome checkclean on all touched files;node --checksyntax-valid.vitestsuite couldn't run here (nonode_modulesin this environment); the change is confined to the save path and the two existingacode.formatcall sites, both audited — no other callers exist.Happy to add unit coverage if there's a preferred mocking pattern for
editorManager-dependent modules.