Skip to content

fix(save): make format-on-save await the formatter and target the saved file - #2915

Open
xaniexane wants to merge 2 commits into
Acode-Foundation:mainfrom
xaniexane:fix/format-on-save-races
Open

xaniexane wants to merge 2 commits into
Acode-Foundation:mainfrom
xaniexane:fix/format-on-save-races

Conversation

@xaniexane

Copy link
Copy Markdown

Problem

Fixes #2900.

Format on Save had three races/corruption vectors in src/lib/saveFile.js:

  1. Wrong file formatted — it toggled editorManager.activeFile.markChanged and 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 provider exists() check during Save As) was pending, the formatter rewrote B while the save still wrote A.
  2. Write before format completedacode.exec("format", false) was fire-and-forget, so an async formatter could finish after the source write, leaving the saved content unformatted.
  3. Failure didn't stop the write — a failed format still wrote the file and reported success.

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), toggle markChanged on the file being saved rather than whatever tab happens to be active, await the format so the write captures the formatted document, and abort the save when the formatter fails.
  • commands.js: the format command now returns the format result so callers can react to it.
  • acode.js: acode.format() returns null (instead of false) 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 check clean on all touched files; node --check syntax-valid.
  • Full vitest suite couldn't run here (no node_modules in this environment); the change is confined to the save path and the two existing acode.format call sites, both audited — no other callers exist.
  • Verified by code inspection against the reproduction steps in Format on Save can format the wrong tab and write before formatting completes #2900: with the guard, step 4 (formatter resolving file B) can no longer happen, and the un-awaited write in step 5 now awaits.

Happy to add unit coverage if there's a preferred mocking pattern for editorManager-dependent modules.

…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
@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

This PR is not safe to merge until formatter failures propagate correctly, asynchronous formatting cannot edit a newly selected tab, and failed Save As operations avoid leaving partial state.

Findings

  1. P1 Tab switch corrupts document
  2. P1 Formatter failures become success
  3. P1 Failed Save As persists

Summary

This PR changes format-on-save to await formatting, conditionally target the file being saved, propagate the format command result, and distinguish an unavailable formatter from formatting failure. The intended sequencing is not yet safe because:

  • Built-in formatter failures are converted to success before reaching the save check.
  • Switching tabs during asynchronous formatting can still apply edits to the wrong in-memory document.
  • Aborting a Save As after formatting failure leaves earlier filesystem and session mutations behind.

Diagram

sequenceDiagram
  participant U as User
  participant S as saveFile
  participant F as Formatter
  participant E as Shared editor
  participant D as File source
  U->>S: Save / Save As
  opt Save As
    S->>D: Create target
    S->>S: Reassign file URI and recents
  end
  S->>F: Await format
  F->>E: Read active document
  F-->>F: Await config/format work
  U->>E: Switch active tab
  F->>E: Dispatch formatted text
  Note over F,E: Dispatch can target the newly active tab
  F-->>S: false may be converted to true
  alt Reported success
    S->>D: Write document
  else Detected failure
    S-->>U: Abort without rolling back Save As
  end
Loading

Reviews (1) · Last reviewed commit: "fix(save): make format-on-save await the..."

Comment thread src/lib/saveFile.js Outdated
file.markChanged = false;
try {
acode.exec("format", false);
if (editorManager.activeFile === file) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Tab switch corrupts document

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:

Comment thread src/lib/saveFile.js Outdated
Comment on lines +186 to +187
const formatted = await acode.exec("format", false);
if (formatted === false) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

Comment thread src/lib/saveFile.js Outdated
// `null` means no formatter is configured, which is not
// a failure.
const formatted = await acode.exec("format", false);
if (formatted === false) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed Save As persists

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

Format on Save can format the wrong tab and write before formatting completes

1 participant