Skip to content

Commit

Permalink
refactor(vscode): move commands and findBinary to separate files (#…
Browse files Browse the repository at this point in the history
…8605)

pure refactor. wanted to add tests but needs to mocks :/
  • Loading branch information
Sysix authored Jan 20, 2025
1 parent 997859c commit 259a47b
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 164 deletions.
2 changes: 1 addition & 1 deletion editors/vscode/client/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class ConfigService implements IDisposable {
private static readonly _namespace = 'oxc';
private readonly _disposables: IDisposable[] = [];

public config: Config;
public readonly config: Config;

public onConfigChange:
| ((this: ConfigService, config: ConfigurationChangeEvent) => void)
Expand Down
112 changes: 112 additions & 0 deletions editors/vscode/client/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { CodeAction, Command, commands, Disposable, window, workspace } from 'vscode';

import { CodeActionRequest, CodeActionTriggerKind, LanguageClient, Position, Range } from 'vscode-languageclient/node';
import { Config } from './Config';

const commandPrefix = 'oxc';

export const enum OxcCommands {
RestartServer = `${commandPrefix}.restartServer`,
ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,
ShowOutputChannel = `${commandPrefix}.showOutputChannel`,
ToggleEnable = `${commandPrefix}.toggleEnable`,
}

export const restartServerCommand = (client: LanguageClient): Disposable => {
return commands.registerCommand(
OxcCommands.RestartServer,
async () => {
if (!client) {
window.showErrorMessage('oxc client not found');
return;
}

try {
if (client.isRunning()) {
await client.restart();

window.showInformationMessage('oxc server restarted.');
} else {
await client.start();
}
} catch (err) {
client.error('Restarting client failed', err, 'force');
}
},
);
};

export const showOutputChannelCommand = (client: LanguageClient): Disposable => {
return commands.registerCommand(
OxcCommands.ShowOutputChannel,
() => {
client.outputChannel.show();
},
);
};

export const toggleEnabledCommand = (config: Config): Disposable => {
return commands.registerCommand(
OxcCommands.ToggleEnable,
() => {
config.updateEnable(!config.enable);
},
);
};

export const applyAllFixesFileCommand = (client: LanguageClient): Disposable => {
return commands.registerCommand(
OxcCommands.ApplyAllFixesFile,
async () => {
if (!client) {
window.showErrorMessage('oxc client not found');
return;
}
const textEditor = window.activeTextEditor;
if (!textEditor) {
window.showErrorMessage('active text editor not found');
return;
}

const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const codeActionResult = await client.sendRequest(CodeActionRequest.type, {
textDocument: {
uri: textEditor.document.uri.toString(),
},
range: Range.create(Position.create(0, 0), lastLine.range.end),
context: {
diagnostics: [],
only: [],
triggerKind: CodeActionTriggerKind.Invoked,
},
});
const commandsOrCodeActions = await client.protocol2CodeConverter.asCodeActionResult(codeActionResult || []);

await Promise.all(
commandsOrCodeActions
.map(async (codeActionOrCommand) => {
// Commands are always applied. Regardless of whether it's a Command or CodeAction#command.
if (isCommand(codeActionOrCommand)) {
await commands.executeCommand(codeActionOrCommand.command, codeActionOrCommand.arguments);
} else {
// Only preferred edits are applied
// LSP states edits must be run first, then commands
if (codeActionOrCommand.edit && codeActionOrCommand.isPreferred) {
await workspace.applyEdit(codeActionOrCommand.edit);
}
if (codeActionOrCommand.command) {
await commands.executeCommand(
codeActionOrCommand.command.command,
codeActionOrCommand.command.arguments,
);
}
}
}),
);

function isCommand(codeActionOrCommand: CodeAction | Command): codeActionOrCommand is Command {
return typeof codeActionOrCommand.command === 'string';
}
},
);
};
182 changes: 20 additions & 162 deletions editors/vscode/client/extension.ts
Original file line number Diff line number Diff line change
@@ -1,188 +1,45 @@
import { promises as fsPromises } from 'node:fs';
import { ExtensionContext, StatusBarAlignment, StatusBarItem, ThemeColor, window, workspace } from 'vscode';

import {
CodeAction,
Command,
commands,
ExtensionContext,
StatusBarAlignment,
StatusBarItem,
ThemeColor,
window,
workspace,
} from 'vscode';

import {
CodeActionRequest,
CodeActionTriggerKind,
Executable,
LanguageClient,
LanguageClientOptions,
MessageType,
Position,
Range,
ServerOptions,
ShowMessageNotification,
} from 'vscode-languageclient';

import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
} from 'vscode-languageclient/node';

import { join } from 'node:path';
import {
applyAllFixesFileCommand,
OxcCommands,
restartServerCommand,
showOutputChannelCommand,
toggleEnabledCommand,
} from './commands';
import { ConfigService } from './ConfigService';
import findBinary from './findBinary';

const languageClientName = 'oxc';
const outputChannelName = 'Oxc';
const commandPrefix = 'oxc';

const enum OxcCommands {
RestartServer = `${commandPrefix}.restartServer`,
ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,
ShowOutputChannel = `${commandPrefix}.showOutputChannel`,
ToggleEnable = `${commandPrefix}.toggleEnable`,
}

let client: LanguageClient;

let myStatusBarItem: StatusBarItem;

export async function activate(context: ExtensionContext) {
const configService = new ConfigService();
const restartCommand = commands.registerCommand(
OxcCommands.RestartServer,
async () => {
if (!client) {
window.showErrorMessage('oxc client not found');
return;
}

try {
if (client.isRunning()) {
await client.restart();

window.showInformationMessage('oxc server restarted.');
} else {
await client.start();
}
} catch (err) {
client.error('Restarting client failed', err, 'force');
}
},
);

const showOutputCommand = commands.registerCommand(
OxcCommands.ShowOutputChannel,
() => {
client?.outputChannel?.show();
},
);

const toggleEnable = commands.registerCommand(
OxcCommands.ToggleEnable,
() => {
configService.config.updateEnable(!configService.config.enable);
},
);

const applyAllFixesFile = commands.registerCommand(
OxcCommands.ApplyAllFixesFile,
async () => {
if (!client) {
window.showErrorMessage('oxc client not found');
return;
}
const textEditor = window.activeTextEditor;
if (!textEditor) {
window.showErrorMessage('active text editor not found');
return;
}

const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1);
const codeActionResult = await client.sendRequest(CodeActionRequest.type, {
textDocument: {
uri: textEditor.document.uri.toString(),
},
range: Range.create(Position.create(0, 0), lastLine.range.end),
context: {
diagnostics: [],
only: [],
triggerKind: CodeActionTriggerKind.Invoked,
},
});
const commandsOrCodeActions = await client.protocol2CodeConverter.asCodeActionResult(codeActionResult || []);

await Promise.all(
commandsOrCodeActions
.map(async (codeActionOrCommand) => {
// Commands are always applied. Regardless of whether it's a Command or CodeAction#command.
if (isCommand(codeActionOrCommand)) {
await commands.executeCommand(codeActionOrCommand.command, codeActionOrCommand.arguments);
} else {
// Only preferred edits are applied
// LSP states edits must be run first, then commands
if (codeActionOrCommand.edit && codeActionOrCommand.isPreferred) {
await workspace.applyEdit(codeActionOrCommand.edit);
}
if (codeActionOrCommand.command) {
await commands.executeCommand(
codeActionOrCommand.command.command,
codeActionOrCommand.command.arguments,
);
}
}
}),
);

function isCommand(codeActionOrCommand: CodeAction | Command): codeActionOrCommand is Command {
return typeof codeActionOrCommand.command === 'string';
}
},
);

context.subscriptions.push(
applyAllFixesFile,
restartCommand,
showOutputCommand,
toggleEnable,
applyAllFixesFileCommand(client),
restartServerCommand(client),
showOutputChannelCommand(client),
toggleEnabledCommand(configService.config),
configService,
);

const outputChannel = window.createOutputChannel(outputChannelName, { log: true });

async function findBinary(): Promise<string> {
let bin = configService.config.binPath;
if (bin) {
try {
await fsPromises.access(bin);
return bin;
} catch {}
}

const workspaceFolders = workspace.workspaceFolders;
const isWindows = process.platform === 'win32';

if (workspaceFolders?.length && !isWindows) {
try {
return await Promise.any(
workspaceFolders.map(async (folder) => {
const binPath = join(
folder.uri.fsPath,
'node_modules',
'.bin',
'oxc_language_server',
);

await fsPromises.access(binPath);
return binPath;
}),
);
} catch {}
}

const ext = isWindows ? '.exe' : '';
// NOTE: The `./target/release` path is aligned with the path defined in .github/workflows/release_vscode.yml
return (
process.env.SERVER_PATH_DEV ??
join(context.extensionPath, `./target/release/oxc_language_server${ext}`)
);
}

const command = await findBinary();
const command = await findBinary(context, configService.config);
const run: Executable = {
command: command!,
options: {
Expand Down Expand Up @@ -232,6 +89,7 @@ export async function activate(context: ExtensionContext) {
serverOptions,
clientOptions,
);

client.onNotification(ShowMessageNotification.type, (params) => {
switch (params.type) {
case MessageType.Debug:
Expand Down
44 changes: 44 additions & 0 deletions editors/vscode/client/findBinary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { promises as fsPromises } from 'node:fs';
import { join } from 'node:path';

import { ExtensionContext, workspace } from 'vscode';

import { Config } from './Config';

export default async function findBinary(context: ExtensionContext, config: Config): Promise<string> {
let bin = config.binPath;
if (bin) {
try {
await fsPromises.access(bin);
return bin;
} catch {}
}

const workspaceFolders = workspace.workspaceFolders;
const isWindows = process.platform === 'win32';

if (workspaceFolders?.length && !isWindows) {
try {
return await Promise.any(
workspaceFolders.map(async (folder) => {
const binPath = join(
folder.uri.fsPath,
'node_modules',
'.bin',
'oxc_language_server',
);

await fsPromises.access(binPath);
return binPath;
}),
);
} catch {}
}

const ext = isWindows ? '.exe' : '';
// NOTE: The `./target/release` path is aligned with the path defined in .github/workflows/release_vscode.yml
return (
process.env.SERVER_PATH_DEV ??
join(context.extensionPath, `./target/release/oxc_language_server${ext}`)
);
}
2 changes: 1 addition & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"server:build:debug": "cargo build -p oxc_language_server",
"server:build:release": "cross-env CARGO_TARGET_DIR=./target cargo build -p oxc_language_server --release",
"lint": "npx oxlint --config=oxlint.json --tsconfig=tsconfig.json",
"test": "esbuild client/config.spec.ts --bundle --outfile=out/config.spec.js --external:vscode --format=cjs --platform=node --target=node16 --minify --sourcemap && vscode-test",
"test": "esbuild \"client/*.spec.ts\" --bundle --outdir=out --external:vscode --format=cjs --platform=node --target=node16 --minify --sourcemap && vscode-test",
"type-check": "tsc --noEmit"
},
"devDependencies": {
Expand Down

0 comments on commit 259a47b

Please sign in to comment.