Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ writeFileSync(multiRootWorkspaceFile, JSON.stringify(multiRootWorkspaceConfig, n
// Use binaries from node_modules
const oxlintBin = path.resolve(import.meta.dirname, "node_modules/.bin/oxlint");
const oxfmtBin = path.resolve(import.meta.dirname, "node_modules/.bin/oxfmt");
const fakeOxlintBin = path.resolve(
import.meta.dirname,
"tests/fixtures/lsp_servers/fake_oxlint_server.js",
);
const fakeOxfmtBin = path.resolve(
import.meta.dirname,
"tests/fixtures/lsp_servers/fake_oxfmt_server.js",
);

const baseTest = {
files: "out_test/integration/**/*.spec.js",
Expand Down Expand Up @@ -47,7 +55,7 @@ const allTestSuites = new Map([
...baseTest,
env: {
SINGLE_FOLDER_WORKSPACE: "true",
SERVER_PATH_DEV: oxlintBin,
SERVER_PATH_DEV_OXLINT: oxlintBin,
SKIP_FORMATTER_TEST: "true",
},
},
Expand All @@ -59,7 +67,7 @@ const allTestSuites = new Map([
workspaceFolder: multiRootWorkspaceFile,
env: {
MULTI_FOLDER_WORKSPACE: "true",
SERVER_PATH_DEV: oxlintBin,
SERVER_PATH_DEV_OXLINT: oxlintBin,
SKIP_FORMATTER_TEST: "true",
},
},
Expand All @@ -73,7 +81,7 @@ const allTestSuites = new Map([
env: {
SINGLE_FOLDER_WORKSPACE: "true",
OXLINT_JS_PLUGIN: "true",
SERVER_PATH_DEV: oxlintBin,
SERVER_PATH_DEV_OXLINT: oxlintBin,
SKIP_FORMATTER_TEST: "true",
},
},
Expand All @@ -84,11 +92,25 @@ const allTestSuites = new Map([
...baseTest,
env: {
SINGLE_FOLDER_WORKSPACE: "true",
SERVER_PATH_DEV: oxfmtBin,
SERVER_PATH_DEV_OXFMT: oxfmtBin,
SKIP_LINTER_TEST: "true",
},
},
],
[
"format-save-path",
{
...baseTest,
files: "out_test/integration/format_on_save_no_lint.spec.js",
env: {
SINGLE_FOLDER_WORKSPACE: "true",
SERVER_PATH_DEV_OXLINT: fakeOxlintBin,
SERVER_PATH_DEV_OXFMT: fakeOxfmtBin,
FAKE_OXLINT_CODE_ACTION_MS: "5000",
FAKE_OXLINT_DIAGNOSTIC_MS: "5000",
},
},
],
]);

export default defineConfig({
Expand Down
23 changes: 20 additions & 3 deletions client/tools/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Uri,
workspace,
} from "vscode";
import type { CodeActionContext } from "vscode";

import {
DocumentFilter,
Expand Down Expand Up @@ -42,6 +43,20 @@ formatCodeAction.command = {
tooltip: "Format the document using the default formatter",
};

export function shouldProvideOxfmtCodeAction(context: CodeActionContext): boolean {
const requestedKind = context.only;
if (requestedKind === undefined) {
return true;
}

// Avoid participating in broad source-action scans. Oxfmt only owns the
// explicit format source action; format-on-save uses the formatter provider.
return (
requestedKind.value !== CodeActionKind.Source.value &&
formatCodeActionKind.intersects(requestedKind)
);
}

// This list is not used as-is for implementation to determine whether formatting processing is possible.
const supportedExtensions = [
"cjs",
Expand Down Expand Up @@ -276,8 +291,9 @@ export default class FormatterTool implements ToolInterface {
outputChannel: LogOutputChannel,
configService: ConfigService,
): Promise<BinarySearchResult | undefined> {
if (process.env.SERVER_PATH_DEV) {
return { path: process.env.SERVER_PATH_DEV, loader: "native" };
if (process.env.SERVER_PATH_DEV_OXFMT) {
const path = process.env.SERVER_PATH_DEV_OXFMT;
return { path, loader: path.endsWith(".js") ? "node" : "native" };
}
const bin = await configService.getOxfmtServerBinPath();
if (bin) {
Expand Down Expand Up @@ -315,9 +331,10 @@ export default class FormatterTool implements ToolInterface {
// @ts-expect-error DocumentFilter/DocumentSelector is not correctly typed, here it expects a readonly array, which we provide.
this.documentSelectors,
{
provideCodeActions: (doc) => {
provideCodeActions: (doc, _range, context) => {
if (
configService.vsCodeConfig.enableOxfmt === false ||
!shouldProvideOxfmtCodeAction(context) ||
workspace.getConfiguration("editor", doc).get("defaultFormatter") !== "oxc.oxc-vscode"
) {
return [];
Expand Down
127 changes: 124 additions & 3 deletions client/tools/linter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { promises as fsPromises } from "node:fs";

import {
CodeActionKind,
CodeActionTriggerKind,
commands,
ConfigurationChangeEvent,
LogOutputChannel,
Uri,
window,
workspace,
} from "vscode";
import type { CodeActionContext, TextDocument } from "vscode";

import {
ConfigurationParams,
Expand Down Expand Up @@ -37,6 +40,99 @@ const enum LspCommands {
}

const oxlintConfigDefaultFilePattern = `**/{.oxlintrc.json,.oxlintrc.jsonc,oxlint.config.ts}`;
const oxlintFixAllCodeActionKind = CodeActionKind.SourceFixAll.append("oxc");
type CodeActionsOnSaveSetting = boolean | "always" | "explicit" | "never";
type CodeActionsOnSave = Record<string, CodeActionsOnSaveSetting | undefined>;
type CodeActionsOnSaveConfiguration = CodeActionsOnSave | string[];

function isEnabledCodeActionsOnSaveSetting(setting: CodeActionsOnSaveSetting | undefined): boolean {
// VS Code accepts legacy boolean values and current string values here.
return setting === true || setting === "always" || setting === "explicit";
}

export function shouldCodeActionsOnSaveRequestOxlint(
codeActionsOnSave: CodeActionsOnSaveConfiguration | undefined,
): boolean {
if (Array.isArray(codeActionsOnSave)) {
return (
codeActionsOnSave.includes(CodeActionKind.Source.value) ||
codeActionsOnSave.includes(CodeActionKind.SourceFixAll.value) ||
codeActionsOnSave.includes(oxlintFixAllCodeActionKind.value)
);
}

if (codeActionsOnSave === undefined) {
return false;
}

const oxlintFixAllSetting = codeActionsOnSave[oxlintFixAllCodeActionKind.value];
if (oxlintFixAllSetting !== undefined) {
// A provider-specific setting is the user's most precise opt-in or opt-out.
return isEnabledCodeActionsOnSaveSetting(oxlintFixAllSetting);
}

const fixAllSetting = codeActionsOnSave[CodeActionKind.SourceFixAll.value];
if (fixAllSetting !== undefined) {
// Generic fix-all is more specific than broad `source`, so it controls
// whether source.fixAll.oxc participates unless the oxc key overrides it.
return isEnabledCodeActionsOnSaveSetting(fixAllSetting);
}

return isEnabledCodeActionsOnSaveSetting(codeActionsOnSave[CodeActionKind.Source.value]);
}

function shouldRunOxlintCodeActionsOnSave(document: TextDocument): boolean {
const codeActionsOnSave = workspace
.getConfiguration("editor", document)
.get<CodeActionsOnSaveConfiguration>("codeActionsOnSave");

return shouldCodeActionsOnSaveRequestOxlint(codeActionsOnSave);
}

export function shouldRequestOxlintCodeActions(
context: CodeActionContext,
codeActionsOnSaveRequestsOxlint: boolean = false,
): boolean {
const requestedKind = context.only;
if (requestedKind === undefined) {
// Empty automatic probes are used for editor UI discovery, not an explicit
// user command or configured fix-all action. Keep diagnostic-bearing
// automatic requests so VS Code can discover oxlint quick fixes.
return (
context.triggerKind !== CodeActionTriggerKind.Automatic || context.diagnostics.length > 0
);
}

const requestedKindValue = requestedKind.value;
// `CodeActionKind.intersects` treats `source.fixAll` as intersecting with
// provider-owned subkinds such as `source.fixAll.biome`. Save participants
// wait for those requests, so only route exact oxlint-owned source actions.
const requestsOxlintKind =
requestedKindValue === CodeActionKind.Source.value ||
requestedKindValue === CodeActionKind.QuickFix.value ||
requestedKindValue.startsWith(`${CodeActionKind.QuickFix.value}.`) ||
requestedKindValue === CodeActionKind.SourceFixAll.value ||
requestedKindValue === oxlintFixAllCodeActionKind.value;

if (!requestsOxlintKind) {
return false;
}

const isAutomaticSaveRunnableSourceAction =
context.triggerKind === CodeActionTriggerKind.Automatic &&
(requestedKindValue === CodeActionKind.Source.value ||
requestedKindValue === CodeActionKind.SourceFixAll.value ||
requestedKindValue === oxlintFixAllCodeActionKind.value);

// Automatic source-action requests are save-runnable. Oxlint should only
// participate in those when save settings enable its fix-all action;
// otherwise the extension would ignore explicit provider opt-outs.
if (isAutomaticSaveRunnableSourceAction && !codeActionsOnSaveRequestsOxlint) {
return false;
}

return true;
}

export default class LinterTool implements ToolInterface {
// Global flag to check if the user allows us to start the server.
Expand All @@ -56,8 +152,9 @@ export default class LinterTool implements ToolInterface {
outputChannel: LogOutputChannel,
configService: ConfigService,
): Promise<BinarySearchResult | undefined> {
if (process.env.SERVER_PATH_DEV) {
return { path: process.env.SERVER_PATH_DEV, loader: "native" };
if (process.env.SERVER_PATH_DEV_OXLINT) {
const path = process.env.SERVER_PATH_DEV_OXLINT;
return { path, loader: path.endsWith(".js") ? "node" : "native" };
}
const bin = await configService.getOxlintServerBinPath();
if (bin) {
Expand Down Expand Up @@ -166,9 +263,33 @@ export default class LinterTool implements ToolInterface {
onChange: true,
onSave: true,
onTabs: false,
filter: (document, mode) => !configService.shouldRequestDiagnostics(document.uri, mode),
filter: (document, mode) => {
if (!configService.shouldRequestDiagnostics(document.uri, mode)) {
return true;
}

return false;
},
},
middleware: {
provideCodeActions: (document, range, context, token, next) => {
const needsCodeActionsOnSaveConfig =
context.triggerKind === CodeActionTriggerKind.Automatic &&
(context.only?.value === CodeActionKind.Source.value ||
context.only?.value === CodeActionKind.SourceFixAll.value ||
context.only?.value === oxlintFixAllCodeActionKind.value);

if (
!shouldRequestOxlintCodeActions(
context,
needsCodeActionsOnSaveConfig && shouldRunOxlintCodeActionsOnSave(document),
)
) {
return [];
}

return next(document, range, context, token);
},
handleDiagnostics: (uri, diagnostics, next) => {
for (const diag of diagnostics) {
// https://github.com/oxc-project/oxc/issues/12404
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
"package": "vsce package --no-dependencies -o oxc_language_server.vsix",
"package:pre-release": "vsce package --no-dependencies --pre-release -o oxc_language_server.vsix",
"install-extension": "code --install-extension oxc_language_server.vsix --force",
"test": "cross-env TEST=true pnpm run compile && vscode-test",
"test:unit": "cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=unit vscode-test",
"test:oxlint": "cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxlint-lsp vscode-test",
"test:oxlint-js": "cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxlint-js vscode-test",
"test:oxlint-multi-root": "cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxlint-lsp-multi-root vscode-test",
"test:oxfmt": "cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxfmt-lsp vscode-test",
"test": "pnpm run compile && cross-env TEST=true pnpm run compile && vscode-test",
"test:unit": "pnpm run compile && cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=unit vscode-test",
"test:oxlint": "pnpm run compile && cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxlint-lsp vscode-test",
"test:oxlint-js": "pnpm run compile && cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxlint-js vscode-test",
"test:oxlint-multi-root": "pnpm run compile && cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxlint-lsp-multi-root vscode-test",
"test:oxfmt": "pnpm run compile && cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=oxfmt-lsp vscode-test",
"test:format-save": "pnpm run compile && cross-env TEST=true pnpm run compile && cross-env TEST_SUITE=format-save-path vscode-test",
"lint": "oxlint -c oxlint.config.json",
"fmt": "oxfmt --write -c oxfmt.config.json",
"fmt:check": "oxfmt --check -c oxfmt.config.json",
Expand Down
Loading
Loading