diff --git a/src/cm/lsp/clientManager.ts b/src/cm/lsp/clientManager.ts index efd9cb284..351745872 100644 --- a/src/cm/lsp/clientManager.ts +++ b/src/cm/lsp/clientManager.ts @@ -538,6 +538,13 @@ export class LspClientManager { plugin.client.sync(); return true; } + // The user may have switched tabs while the formatting request was + // in flight; never apply this file's edits to another file's + // document. Abort without writing instead. + const em = (globalThis as Record).editorManager as + | { activeFile?: unknown } + | undefined; + if (em && file && em.activeFile !== file) return false; const applied = applyTextEdits(plugin, view, edits); if (applied) { plugin.client.sync(); diff --git a/src/lib/acode.js b/src/lib/acode.js index f36c7401a..39a447f62 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -826,6 +826,13 @@ class Acode { appSettings.update(false); } + /** + * Formats the active file with the configured formatter. + * @param {boolean} [selectIfNull] open the formatter picker when no formatter is configured + * @returns {Promise} `true` when formatting ran, `false` + * when the formatter failed, `null` when no formatter is configured + * (not a failure; callers should not treat it as one) + */ async format(selectIfNull = true) { const file = editorManager.activeFile; if (!file || file.type !== "editor") return false; @@ -858,12 +865,22 @@ class Acode { } else { toast(strings["please select a formatter"]); } - return false; + // No formatter configured: not a failure, so report `null` + // instead of `false` (both are falsy for existing callers). + return null; } try { - await formatter.format(); - return true; + // Propagate the formatter's own result instead of assuming + // success: formatters resolve `false` for handled failures + // (e.g. Prettier failing to parse, LSP reporting failure). + const didFormat = (await formatter.format()) !== false; + // The formatter dispatches through the shared editor view; if + // the user switched tabs while it was awaiting, its edits may + // have targeted the wrong document, so report failure instead + // of success and let callers abort. + if (editorManager.activeFile !== file) return false; + return didFormat; } catch (error) { helpers.error(error); return false; diff --git a/src/lib/commands.js b/src/lib/commands.js index de3a005a8..efbcbe51c 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -727,6 +727,7 @@ export default { // Restore cursor position after formatting (pos.row is now 1-based) editor.gotoLine(pos.row, pos.column); } + return didFormat; }, async eol() { if (editorManager.activeFile?.type !== "editor") return; diff --git a/src/lib/prettierFormatter.js b/src/lib/prettierFormatter.js index 7200e5646..505a217b9 100644 --- a/src/lib/prettierFormatter.js +++ b/src/lib/prettierFormatter.js @@ -98,6 +98,11 @@ export async function formatActiveFileWithPrettier() { if (formatted === source) return true; + // The user may have switched tabs while the config/format awaits + // resolved; never dispatch into a document that is no longer this + // file's. Abort without writing instead. + if (editorManager?.activeFile !== file) return false; + editor.dispatch({ changes: { from: 0, diff --git a/src/lib/saveFile.js b/src/lib/saveFile.js index 0f43feaac..f42e33d11 100644 --- a/src/lib/saveFile.js +++ b/src/lib/saveFile.js @@ -117,6 +117,29 @@ async function saveFile( let saved = false; try { + if (appSettings.value.formatOnSave) { + // Format the in-memory content BEFORE creating/reassigning the + // save target below: a failed format aborts the save before any + // file creation, URI reassignment, or recents update happens. + // Formatters operate on the active editor document. If the user + // switched tabs while an async save step was pending, formatting + // here would rewrite the wrong file, so only format when the file + // being saved is the active one. + file.markChanged = false; + try { + if (editorManager.activeFile === file) { + // Await the formatter so the write below captures the + // formatted document. A failed format aborts the save; + // `null` means no formatter is configured, which is not + // a failure. + const formatted = await acode.exec("format", false); + if (formatted === false) return false; + } + } finally { + file.markChanged = true; + } + } + if (isSaveAs || newUrl) { // if save as or new file const fileUri = Url.join(newUrl, file.filename); @@ -171,15 +194,6 @@ async function saveFile( } } - if (appSettings.value.formatOnSave) { - editorManager.activeFile.markChanged = false; - try { - acode.exec("format", false); - } finally { - editorManager.activeFile.markChanged = true; - } - } - const savedDoc = file.session?.doc || null; const savedVersion = file.docVersion; const data = getDocText(savedDoc);