diff --git a/editors/vscode/client/config.ts b/editors/vscode/client/config.ts new file mode 100644 index 0000000000000..72da458a4406a --- /dev/null +++ b/editors/vscode/client/config.ts @@ -0,0 +1,161 @@ +import { ConfigurationChangeEvent, workspace, WorkspaceConfiguration } from 'vscode'; +import { IDisposable } from './types'; + +export class ConfigService implements Config, IDisposable { + private static readonly _namespace = 'oxc'; + private readonly _disposables: IDisposable[] = []; + private _inner: WorkspaceConfiguration; + private _runTrigger: 'onSave' | 'onType'; + private _enable: boolean; + private _trace: 'off' | 'messages' | 'verbose'; + private _configPath: string; + private _binPath: string | undefined; + + public onConfigChange: + | ((this: ConfigService, config: ConfigurationChangeEvent) => void) + | undefined; + + constructor() { + this._inner = workspace.getConfiguration(ConfigService._namespace); + this._runTrigger = this._inner.get('lint.run') || 'onType'; + this._enable = this._inner.get('enable') ?? true; + this._trace = this._inner.get('trace.server') || 'off'; + this._configPath = this._inner.get('configPath') || '.eslintrc'; + this._binPath = this._inner.get('path.server'); + this.onConfigChange = undefined; + + const disposeChangeListener = workspace.onDidChangeConfiguration( + this.onVscodeConfigChange.bind(this), + ); + this._disposables.push(disposeChangeListener); + } + + get rawConfig(): WorkspaceConfiguration { + return this._inner; + } + + get runTrigger(): Trigger { + return this._runTrigger; + } + + set runTrigger(value: Trigger) { + this._runTrigger = value; + workspace + .getConfiguration(ConfigService._namespace) + .update('lint.run', value); + } + + get enable(): boolean { + return this._enable; + } + + set enable(value: boolean) { + this._enable = value; + workspace + .getConfiguration(ConfigService._namespace) + .update('enable', value); + } + + get trace(): TraceLevel { + return this._trace; + } + + set trace(value: TraceLevel) { + this._trace = value; + workspace + .getConfiguration(ConfigService._namespace) + .update('trace.server', value); + } + + get configPath(): string { + return this._configPath; + } + + set configPath(value: string) { + this._configPath = value; + workspace + .getConfiguration(ConfigService._namespace) + .update('configPath', value); + } + + get binPath(): string | undefined { + return this._binPath; + } + + set binPath(value: string | undefined) { + this._binPath = value; + workspace + .getConfiguration(ConfigService._namespace) + .update('path.server', value); + } + + private onVscodeConfigChange(event: ConfigurationChangeEvent): void { + if (event.affectsConfiguration(ConfigService._namespace)) { + this._runTrigger = this._inner.get('lint.run') || 'onType'; + this._enable = this._inner.get('enable') ?? true; + this._trace = this._inner.get('trace.server') || 'off'; + this._configPath = this._inner.get('configPath') || '.eslintrc'; + this._binPath = this._inner.get('path.server'); + this.onConfigChange?.call(this, event); + } + } + + dispose() { + for (const disposable of this._disposables) { + disposable.dispose(); + } + } + + public toJSON(): Config { + return { + runTrigger: this.runTrigger, + enable: this.enable, + trace: this.trace, + configPath: this.configPath, + binPath: this.binPath, + }; + } +} + +type Trigger = 'onSave' | 'onType'; +type TraceLevel = 'off' | 'messages' | 'verbose'; + +/** + * See `"contributes.configuration"` in `package.json` + */ +interface Config { + /** + * When to run the linter and generate diagnostics + * `oxc.lint.run` + * + * @default 'onType' + */ + runTrigger: Trigger; + /** + * `oxc.enable` + * + * @default true + */ + enable: boolean; + /** + * Trace VSCode <-> Oxc Language Server communication + * `oxc.trace.server` + * + * @default 'off' + */ + trace: TraceLevel; + /** + * oxlint config path + * + * `oxc.configPath` + * + * @default ".eslintrc" + */ + configPath: string; + /** + * Path to LSP binary + * `oxc.path.server` + * @default undefined + */ + binPath: string | undefined; +} diff --git a/editors/vscode/client/extension.ts b/editors/vscode/client/extension.ts index 043784983c0eb..036443de4159c 100644 --- a/editors/vscode/client/extension.ts +++ b/editors/vscode/client/extension.ts @@ -1,19 +1,11 @@ import { promises as fsPromises } from 'node:fs'; -import { - commands, - ConfigurationTarget, - ExtensionContext, - StatusBarAlignment, - StatusBarItem, - ThemeColor, - window, - workspace, -} from 'vscode'; +import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, ThemeColor, window, workspace } from 'vscode'; import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; import { join } from 'node:path'; +import { ConfigService } from './config'; const languageClientId = 'oxc-vscode'; const languageClientName = 'oxc'; @@ -34,6 +26,7 @@ let client: LanguageClient; let myStatusBarItem: StatusBarItem; export async function activate(context: ExtensionContext) { + const config = new ConfigService(); const restartCommand = commands.registerCommand( OxcCommands.RestartServer, async () => { @@ -73,13 +66,7 @@ export async function activate(context: ExtensionContext) { const toggleEnable = commands.registerCommand( OxcCommands.ToggleEnable, () => { - let enabled = workspace - .getConfiguration('oxc_language_server') - .get('enable'); - let nextState = !enabled; - workspace - .getConfiguration('oxc_language_server') - .update('enable', nextState, ConfigurationTarget.Global); + config.enable = !config.enable; }, ); @@ -88,15 +75,14 @@ export async function activate(context: ExtensionContext) { showOutputCommand, showTraceOutputCommand, toggleEnable, + config, ); const outputChannel = window.createOutputChannel(outputChannelName); const traceOutputChannel = window.createOutputChannel(traceOutputChannelName); async function findBinary(): Promise { - const cfg = workspace.getConfiguration('oxc'); - - let bin = cfg.get('binPath', ''); + let bin = config.binPath; if (bin) { try { await fsPromises.access(bin); @@ -150,9 +136,7 @@ export async function activate(context: ExtensionContext) { // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used // Options to control the language client - let clientConfig: any = JSON.parse( - JSON.stringify(workspace.getConfiguration('oxc_language_server')), - ); + let clientConfig: any = JSON.parse(JSON.stringify(config.rawConfig)); let clientOptions: LanguageClientOptions = { // Register the server for plain text documents documentSelector: [ @@ -191,17 +175,11 @@ export async function activate(context: ExtensionContext) { }); }); - workspace.onDidChangeConfiguration((e) => { - let isAffected = e.affectsConfiguration('oxc_language_server'); - if (!isAffected) { - return; - } - let settings: any = JSON.parse( - JSON.stringify(workspace.getConfiguration('oxc_language_server')), - ); + config.onConfigChange = function onConfigChange() { + let settings: any = JSON.parse(JSON.stringify(this)); updateStatsBar(settings.enable); client.sendNotification('workspace/didChangeConfiguration', { settings }); - }); + }; function updateStatsBar(enable: boolean) { if (!myStatusBarItem) { diff --git a/editors/vscode/client/types.ts b/editors/vscode/client/types.ts new file mode 100644 index 0000000000000..80297c476ab77 --- /dev/null +++ b/editors/vscode/client/types.ts @@ -0,0 +1,9 @@ +/** + * A type with a destructor for releasing resources when de-registered by an LSP client. + * + * There's a newer {@link Disposable} interface that works with `using`, but + * VSCode uses this in its APIs. + */ +export interface IDisposable { + dispose(): void | Promise; +} diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 003acc975ef4e..9119ad4720f95 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -100,6 +100,11 @@ "scope": "window", "default": ".eslintrc", "description": "Path to ESlint configuration." + }, + "oxc.path.server": { + "type": "string", + "scope": "window", + "description": "Path to Oxc language server binary." } } },