From e6bf267c82ed7e87fd33999ea28621351f220ce9 Mon Sep 17 00:00:00 2001 From: panudetjt <3482285+panudetjt@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:42:06 +0700 Subject: [PATCH] fix(formatter): fix formatting order in codeActionsOnSave Ensure formatting is applied via onWillSaveTextDocument before code actions are collected when source.format.oxc is configured in editor.codeActionsOnSave. This guarantees format-before-lint order and prevents the formatter from undoing lint fixes. Suppress the source.format.oxc code action when it's already handled by onWillSaveTextDocument to avoid duplicate formatting. --- client/tools/formatter.ts | 62 ++++++++++++++++++++++++++ tests/integration/code_actions.spec.ts | 53 +++++++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/client/tools/formatter.ts b/client/tools/formatter.ts index ab5b60f..06c9aa2 100644 --- a/client/tools/formatter.ts +++ b/client/tools/formatter.ts @@ -7,6 +7,8 @@ import { ConfigurationChangeEvent, languages, LogOutputChannel, + Range, + TextEdit, Uri, workspace, } from "vscode"; @@ -321,6 +323,18 @@ export default class FormatterTool implements ToolInterface { ) { return []; } + + // When source.format.oxc is configured in codeActionsOnSave, + // formatting is handled by the onWillSaveTextDocument listener + // which runs BEFORE code actions are collected. Returning a + // code action with a command here would run the formatter + // AFTER lint fixes, undoing them. + const codeActionsOnSave = workspace.getConfiguration("editor", doc) + .get>("codeActionsOnSave"); + if (codeActionsOnSave?.["source.format.oxc"]) { + return []; + } + return [formatCodeAction]; }, }, @@ -329,6 +343,53 @@ export default class FormatterTool implements ToolInterface { }, ); + // Apply formatting via onWillSaveTextDocument so that edits are in the + // document BEFORE codeActionsOnSave are collected. This guarantees the + // format→lint order when both source.format.oxc and source.fixAll.oxc + // are configured in editor.codeActionsOnSave. + const onWillSave = workspace.onWillSaveTextDocument(async (event) => { + if ( + configService.vsCodeConfig.enableOxfmt === false || + !this.client + ) { + return; + } + + const codeActionsOnSave = workspace.getConfiguration("editor", event.document) + .get>("codeActionsOnSave"); + + // Only apply formatting via this listener when source.format.oxc is + // configured in codeActionsOnSave. + if (!codeActionsOnSave?.["source.format.oxc"]) { + return; + } + + try { + const edits = await this.client.sendRequest< + { range: { start: { line: number; character: number }; end: { line: number; character: number } }; newText: string }[] + >("textDocument/formatting", { + textDocument: { uri: event.document.uri.toString() }, + options: { tabSize: 2, insertSpaces: true }, + }); + + if (edits) { + event.waitUntil(Promise.resolve( + edits.map((edit) => new TextEdit( + new Range( + edit.range.start.line, + edit.range.start.character, + edit.range.end.line, + edit.range.end.character, + ), + edit.newText, + )), + )); + } + } catch { + // Formatting failed – let the normal formatOnSave or code action handle it + } + }); + outputChannel.info(`Using server binary at: ${binary?.path}`); const run: Executable = runExecutable( @@ -386,6 +447,7 @@ export default class FormatterTool implements ToolInterface { restartCommand.dispose(); toggleEnable.dispose(); formatAction.dispose(); + onWillSave.dispose(); onNotificationDispose.dispose(); }; diff --git a/tests/integration/code_actions.spec.ts b/tests/integration/code_actions.spec.ts index bdf6847..778a415 100644 --- a/tests/integration/code_actions.spec.ts +++ b/tests/integration/code_actions.spec.ts @@ -226,8 +226,56 @@ suite("code actions formatter", () => { return; } - // TODO: re-enable this test after fixing the underlying issue of code actions on save not being triggered in tests - test.skip("code action `source.format.oxc` on editor.codeActionsOnSave", async () => { + test("code action `source.format.oxc` is suppressed when configured in codeActionsOnSave", async () => { + await workspace.getConfiguration("editor").update("defaultFormatter", "oxc.oxc-vscode"); + await workspace.getConfiguration("editor").update("codeActionsOnSave", { + "source.format.oxc": "always", + }); + await workspace.saveAll(); + await loadFixture("formatting"); + + await sleep(500); + + const fileUri = Uri.joinPath(fixturesWorkspaceUri(), "fixtures", "formatting.ts"); + await window.showTextDocument(fileUri); + + const codeActions = await commands.executeCommand( + "vscode.executeCodeActionProvider", + fileUri, + new Range(new Position(0, 0), new Position(0, 0)), + ) as CodeAction[]; + + assert(Array.isArray(codeActions)); + const formatActions = codeActions.filter((a) => a.kind?.value === "source.format.oxc"); + strictEqual(formatActions.length, 0, "should return no format code action when source.format.oxc is in codeActionsOnSave"); + }); + + test("code action `source.format.oxc` is returned when NOT configured in codeActionsOnSave", async () => { + await workspace.getConfiguration("editor").update("defaultFormatter", "oxc.oxc-vscode"); + await workspace.getConfiguration("editor").update("codeActionsOnSave", undefined); + await workspace.saveAll(); + await loadFixture("formatting"); + + await sleep(500); + + const fileUri = Uri.joinPath(fixturesWorkspaceUri(), "fixtures", "formatting.ts"); + await window.showTextDocument(fileUri); + + const codeActions = await commands.executeCommand( + "vscode.executeCodeActionProvider", + fileUri, + new Range(new Position(0, 0), new Position(0, 0)), + ) as CodeAction[]; + + assert(Array.isArray(codeActions)); + const formatActions = codeActions.filter((a) => a.kind?.value === "source.format.oxc"); + strictEqual(formatActions.length, 1, "should return format code action when source.format.oxc is NOT in codeActionsOnSave"); + strictEqual(formatActions[0].title, "Format Document"); + }); + + // TODO: re-enable after fixing the test infrastructure issue where + // onWillSaveTextDocument edits are not applied during save in the test environment + test.skip("code action `source.format.oxc` on editor.codeActionsOnSave applies formatting on save", async () => { await workspace.getConfiguration("editor").update("defaultFormatter", "oxc.oxc-vscode"); await workspace.getConfiguration("editor").update("codeActionsOnSave", { "source.format.oxc": "always", @@ -239,6 +287,7 @@ suite("code actions formatter", () => { const fileUri = Uri.joinPath(fixturesWorkspaceUri(), "fixtures", "formatting.ts"); + // Dirty the file by making a small edit const range = new Range(new Position(0, 0), new Position(0, 0)); const edit = new WorkspaceEdit(); edit.replace(fileUri, range, " ");