From cb98410b0ce02e5e40a7cbf822b8a72a344b3b28 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 20 Dec 2024 14:46:40 -0500 Subject: [PATCH 1/5] Send structured `InitializationOptions` to the server In particular, this lets us respect `air.logLevel` and `air.dependencyLogLevels` on server startup User and workspace level settings are not used yet but are included for completeness --- .vscode/settings.json | 4 ++ crates/lsp/src/handlers_state.rs | 21 +++++-- crates/lsp/src/lib.rs | 1 + crates/lsp/src/main_loop.rs | 12 +++- crates/lsp/src/settings.rs | 53 ++++++++++++++++ editors/code/package-lock.json | 59 +++++++++++++++++- editors/code/package.json | 29 +++++++++ editors/code/src/common/README.md | 4 ++ editors/code/src/common/constants.ts | 12 ++++ editors/code/src/common/log/logging.ts | 29 +++++++++ editors/code/src/common/setup.ts | 19 ++++++ editors/code/src/common/vscodeapi.ts | 53 ++++++++++++++++ editors/code/src/lsp.ts | 36 +++++++++-- editors/code/src/settings.ts | 85 ++++++++++++++++++++++++++ 14 files changed, 406 insertions(+), 11 deletions(-) create mode 100644 crates/lsp/src/settings.rs create mode 100644 editors/code/src/common/README.md create mode 100644 editors/code/src/common/constants.ts create mode 100644 editors/code/src/common/log/logging.ts create mode 100644 editors/code/src/common/setup.ts create mode 100644 editors/code/src/common/vscodeapi.ts create mode 100644 editors/code/src/settings.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 22d345c8..c14b79d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,10 @@ "editor.formatOnSaveMode": "file", "editor.defaultFormatter": "rust-lang.rust-analyzer" }, + "[typescript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "rust-analyzer.check.command": "clippy", "rust-analyzer.imports.prefix": "crate", "rust-analyzer.imports.granularity.group": "item", diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs index a570cf31..9a6c890f 100644 --- a/crates/lsp/src/handlers_state.rs +++ b/crates/lsp/src/handlers_state.rs @@ -36,6 +36,7 @@ use crate::documents::Document; use crate::logging; use crate::logging::LogMessageSender; use crate::main_loop::LspState; +use crate::settings::InitializationOptions; use crate::state::workspace_uris; use crate::state::WorldState; @@ -65,17 +66,27 @@ pub(crate) fn initialize( state: &mut WorldState, log_tx: LogMessageSender, ) -> anyhow::Result { - // TODO: Get user specified options from `params.initialization_options` - let log_level = None; - let dependency_log_levels = None; + let InitializationOptions { + global_settings, + user_settings, + workspace_settings, + } = match params.initialization_options { + Some(initialization_options) => InitializationOptions::from_value(initialization_options), + None => InitializationOptions::default(), + }; logging::init_logging( log_tx, - log_level, - dependency_log_levels, + global_settings.log_level, + global_settings.dependency_log_levels, params.client_info.as_ref(), ); + // TODO: Should these be "pulled" using the LSP server->client `configuration()` + // request instead? + lsp_state.user_client_settings = user_settings; + lsp_state.workspace_client_settings = workspace_settings; + // Defaults to UTF-16 let mut position_encoding = None; diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 65451b9f..9e0c46ef 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -15,6 +15,7 @@ pub mod handlers_state; pub mod logging; pub mod main_loop; pub mod rust_analyzer; +pub mod settings; pub mod state; pub mod to_proto; pub mod tower_lsp; diff --git a/crates/lsp/src/main_loop.rs b/crates/lsp/src/main_loop.rs index 780a635b..5bc48b3e 100644 --- a/crates/lsp/src/main_loop.rs +++ b/crates/lsp/src/main_loop.rs @@ -26,6 +26,8 @@ use crate::handlers_state; use crate::handlers_state::ConsoleInputs; use crate::logging::LogMessageSender; use crate::logging::LogState; +use crate::settings::ClientSettings; +use crate::settings::ClientWorkspaceSettings; use crate::state::WorldState; use crate::tower_lsp::LspMessage; use crate::tower_lsp::LspNotification; @@ -145,9 +147,15 @@ pub(crate) struct GlobalState { log_tx: Option, } -/// Unlike `WorldState`, `ParserState` cannot be cloned and is only accessed by +/// Unlike `WorldState`, `LspState` cannot be cloned and is only accessed by /// exclusive handlers. pub(crate) struct LspState { + /// User level [`ClientSettings`] sent over from the client + pub(crate) user_client_settings: ClientSettings, + + /// Workspace level [`ClientSettings`] sent over from the client + pub(crate) workspace_client_settings: Vec, + /// The negociated encoding for document positions. Note that documents are /// always stored as UTF-8 in Rust Strings. This encoding is only used to /// translate UTF-16 positions sent by the client to UTF-8 ones. @@ -165,6 +173,8 @@ pub(crate) struct LspState { impl Default for LspState { fn default() -> Self { Self { + user_client_settings: ClientSettings::default(), + workspace_client_settings: Vec::new(), // Default encoding specified in the LSP protocol position_encoding: PositionEncoding::Wide(WideEncoding::Utf16), parsers: Default::default(), diff --git a/crates/lsp/src/settings.rs b/crates/lsp/src/settings.rs new file mode 100644 index 00000000..456cf6ba --- /dev/null +++ b/crates/lsp/src/settings.rs @@ -0,0 +1,53 @@ +use serde::Deserialize; +use serde_json::Value; +use url::Url; + +// These settings are only needed once, typically for initialization. +// They are read at the global scope on the client side and are never refreshed. +#[derive(Debug, Deserialize, Default, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClientGlobalSettings { + pub(crate) log_level: Option, + pub(crate) dependency_log_levels: Option, +} + +/// This is a direct representation of the user level settings schema sent +/// by the client. It is refreshed after configuration changes. +#[derive(Debug, Deserialize, Default, Clone)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClientSettings {} + +/// This is a direct representation of the workspace level settings schema sent by the +/// client. It is the same as the user level settings with the addition of the workspace +/// path. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClientWorkspaceSettings { + pub(crate) url: Url, + #[serde(flatten)] + pub(crate) settings: ClientSettings, +} + +/// This is the exact schema for initialization options sent in by the client +/// during initialization. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct InitializationOptions { + pub(crate) global_settings: ClientGlobalSettings, + pub(crate) user_settings: ClientSettings, + pub(crate) workspace_settings: Vec, +} + +impl InitializationOptions { + pub(crate) fn from_value(value: Value) -> Self { + serde_json::from_value(value) + .map_err(|err| { + tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings."); + }) + .unwrap_or_default() + } +} diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 64d18a7a..9e8cd07a 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -11,11 +11,13 @@ "dependencies": { "@types/p-queue": "^3.1.0", "adm-zip": "^0.5.16", + "fs-extra": "^11.2.0", "p-queue": "npm:@esm2cjs/p-queue@^7.3.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/adm-zip": "^0.5.6", + "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.9", "@types/node": "20.x", "@types/vscode": "^1.90.0", @@ -467,6 +469,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -481,6 +494,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", @@ -2115,6 +2138,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2215,7 +2252,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -2692,6 +2728,18 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4234,6 +4282,15 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/editors/code/package.json b/editors/code/package.json index 7166ddc6..16ffdfa9 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -6,6 +6,10 @@ "publisher": "Posit", "license": "MIT", "repository": "https://github.com/posit-dev/air", + "serverInfo": { + "name": "Air", + "module": "air" + }, "engines": { "vscode": "^1.90.0" }, @@ -35,6 +39,29 @@ ] } ], + "configuration": { + "properties": { + "air.logLevel": { + "default": null, + "markdownDescription": "Controls the log level of the language server.\n\n**This setting requires a restart to take effect.**", + "enum": [ + "error", + "warning", + "info", + "debug", + "trace" + ], + "scope": "application", + "type": "string" + }, + "air.dependencyLogLevels": { + "default": null, + "markdownDescription": "Controls the log level of the Rust crates that the language server depends on.\n\n**This setting requires a restart to take effect.**", + "scope": "application", + "type": "string" + } + } + }, "configurationDefaults": { "[r]": { "editor.defaultFormatter": "Posit.air" @@ -88,10 +115,12 @@ "@types/p-queue": "^3.1.0", "p-queue": "npm:@esm2cjs/p-queue@^7.3.0", "adm-zip": "^0.5.16", + "fs-extra": "^11.2.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/adm-zip": "^0.5.6", + "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.9", "@types/node": "20.x", "@types/vscode": "^1.90.0", diff --git a/editors/code/src/common/README.md b/editors/code/src/common/README.md new file mode 100644 index 00000000..31da5b8a --- /dev/null +++ b/editors/code/src/common/README.md @@ -0,0 +1,4 @@ +This directory contains "common" utilities used across language server extensions. + +They are pulled directly from this MIT licensed repo: +https://github.com/microsoft/vscode-python-tools-extension-template diff --git a/editors/code/src/common/constants.ts b/editors/code/src/common/constants.ts new file mode 100644 index 00000000..11fffe6f --- /dev/null +++ b/editors/code/src/common/constants.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// https://github.com/microsoft/vscode-python-tools-extension-template + +import * as path from "path"; + +const folderName = path.basename(__dirname); + +export const EXTENSION_ROOT_DIR = + folderName === "common" + ? path.dirname(path.dirname(__dirname)) + : path.dirname(__dirname); diff --git a/editors/code/src/common/log/logging.ts b/editors/code/src/common/log/logging.ts new file mode 100644 index 00000000..45f62f23 --- /dev/null +++ b/editors/code/src/common/log/logging.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// https://github.com/microsoft/vscode-python-tools-extension-template + +import * as util from "util"; +import { Disposable, OutputChannel } from "vscode"; + +type Arguments = unknown[]; +class OutputChannelLogger { + constructor(private readonly channel: OutputChannel) {} + + public traceLog(...data: Arguments): void { + this.channel.appendLine(util.format(...data)); + } +} + +let channel: OutputChannelLogger | undefined; +export function registerLogger(outputChannel: OutputChannel): Disposable { + channel = new OutputChannelLogger(outputChannel); + return { + dispose: () => { + channel = undefined; + }, + }; +} + +export function traceLog(...args: Arguments): void { + channel?.traceLog(...args); +} diff --git a/editors/code/src/common/setup.ts b/editors/code/src/common/setup.ts new file mode 100644 index 00000000..abd33a6d --- /dev/null +++ b/editors/code/src/common/setup.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// https://github.com/microsoft/vscode-python-tools-extension-template + +import * as path from "path"; +import * as fs from "fs-extra"; +import { EXTENSION_ROOT_DIR } from "./constants"; + +export interface IServerInfo { + name: string; + module: string; +} + +export function loadServerDefaults(): IServerInfo { + const packageJson = path.join(EXTENSION_ROOT_DIR, "package.json"); + const content = fs.readFileSync(packageJson).toString(); + const config = JSON.parse(content); + return config.serverInfo as IServerInfo; +} diff --git a/editors/code/src/common/vscodeapi.ts b/editors/code/src/common/vscodeapi.ts new file mode 100644 index 00000000..e43f109f --- /dev/null +++ b/editors/code/src/common/vscodeapi.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// https://github.com/microsoft/vscode-python-tools-extension-template + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + commands, + ConfigurationScope, + Disposable, + LogOutputChannel, + Uri, + window, + workspace, + WorkspaceConfiguration, + WorkspaceFolder, +} from "vscode"; + +export function createOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); +} + +export function getConfiguration( + config: string, + scope?: ConfigurationScope +): WorkspaceConfiguration { + return workspace.getConfiguration(config, scope); +} + +export function registerCommand( + command: string, + callback: (...args: any[]) => any, + thisArg?: any +): Disposable { + return commands.registerCommand(command, callback, thisArg); +} + +export const { onDidChangeConfiguration } = workspace; + +export function isVirtualWorkspace(): boolean { + const isVirtual = + workspace.workspaceFolders && + workspace.workspaceFolders.every((f) => f.uri.scheme !== "file"); + return !!isVirtual; +} + +export function getWorkspaceFolders(): readonly WorkspaceFolder[] { + return workspace.workspaceFolders ?? []; +} + +export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { + return workspace.getWorkspaceFolder(uri); +} diff --git a/editors/code/src/lsp.ts b/editors/code/src/lsp.ts index f37fc43c..5d691f05 100644 --- a/editors/code/src/lsp.ts +++ b/editors/code/src/lsp.ts @@ -1,6 +1,14 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import { default as PQueue } from "p-queue"; +import { IServerInfo, loadServerDefaults } from "./common/setup"; +import { registerLogger, traceLog } from "./common/log/logging"; +import { + getGlobalSettings, + getUserSettings, + getWorkspaceSettings, + IInitializationOptions, +} from "./settings"; // All session management operations are put on a queue. They can't run // concurrently and either result in a started or stopped state. Starting when @@ -14,6 +22,8 @@ enum State { export class Lsp { public client: lc.LanguageClient | null = null; + private serverInfo: IServerInfo; + // We use the same output channel for all LSP instances (e.g. a new instance // after a restart) to avoid having multiple channels in the Output viewpane. private channel: vscode.OutputChannel; @@ -23,7 +33,8 @@ export class Lsp { constructor(context: vscode.ExtensionContext) { this.channel = vscode.window.createOutputChannel("Air Language Server"); - context.subscriptions.push(this.channel); + context.subscriptions.push(this.channel, registerLogger(this.channel)); + this.serverInfo = loadServerDefaults(); this.stateQueue = new PQueue({ concurrency: 1 }); } @@ -52,7 +63,23 @@ export class Lsp { return; } - let options: lc.ServerOptions = { + // Log server information + traceLog(`Name: ${this.serverInfo.name}`); + traceLog(`Module: ${this.serverInfo.module}`); + + const globalSettings = await getGlobalSettings(this.serverInfo.module); + const userSettings = await getUserSettings(this.serverInfo.module); + const workspaceSettings = await getWorkspaceSettings( + this.serverInfo.module + ); + + const initializationOptions: IInitializationOptions = { + globalSettings, + userSettings, + workspaceSettings, + }; + + let serverOptions: lc.ServerOptions = { command: "air", args: ["language-server"], }; @@ -71,13 +98,14 @@ export class Lsp { vscode.workspace.createFileSystemWatcher("**/*.[Rr]"), }, outputChannel: this.channel, + initializationOptions: initializationOptions, }; const client = new lc.LanguageClient( "airLanguageServer", "Air Language Server", - options, - clientOptions, + serverOptions, + clientOptions ); await client.start(); diff --git a/editors/code/src/settings.ts b/editors/code/src/settings.ts new file mode 100644 index 00000000..bfe4692f --- /dev/null +++ b/editors/code/src/settings.ts @@ -0,0 +1,85 @@ +import { WorkspaceConfiguration, WorkspaceFolder } from "vscode"; +import { getConfiguration, getWorkspaceFolders } from "./common/vscodeapi"; + +type LogLevel = "error" | "warn" | "info" | "debug" | "trace"; + +// One time settings that aren't ever refreshed within the extension's lifetime. +// They are read at the user (i.e. global) scope. +export interface IGlobalSettings { + logLevel?: LogLevel; + dependencyLogLevels?: string; +} + +// Client representation of user level client settings. +// TODO: These are refreshed using a `Configuration` LSP request from the server. +// (It is possible we should ONLY get these through `Configuration` and not through +// initializationOptions) +export interface ISettings {} + +// Client representation of workspace level client settings. +// Same as the user level settings, with the addition of the workspace path. +// TODO: These are refreshed using a `Configuration` LSP request from the server. +// (It is possible we should ONLY get these through `Configuration` and not through +// initializationOptions) +export interface IWorkspaceSettings { + url: string; + settings: ISettings; +} + +// This is a direct representation of the Client settings sent to the Server in the +// `initializationOptions` field of `InitializeParams` +export type IInitializationOptions = { + globalSettings: IGlobalSettings; + userSettings: ISettings; + workspaceSettings: IWorkspaceSettings[]; +}; + +export async function getGlobalSettings( + namespace: string +): Promise { + const config = getConfiguration(namespace); + + return { + logLevel: getOptionalUserValue(config, "logLevel"), + dependencyLogLevels: getOptionalUserValue( + config, + "dependencyLogLevels" + ), + }; +} + +export async function getUserSettings(namespace: string): Promise { + const config = getConfiguration(namespace); + + return {}; +} + +export function getWorkspaceSettings( + namespace: string +): Promise { + return Promise.all( + getWorkspaceFolders().map((workspaceFolder) => + getWorkspaceFolderSettings(namespace, workspaceFolder) + ) + ); +} + +async function getWorkspaceFolderSettings( + namespace: string, + workspace: WorkspaceFolder +): Promise { + const config = getConfiguration(namespace, workspace.uri); + + return { + url: workspace.uri.toString(), + settings: {}, + }; +} + +function getOptionalUserValue( + config: WorkspaceConfiguration, + key: string +): T | undefined { + const inspect = config.inspect(key); + return inspect?.globalValue; +} From f7f6e08966109d0735f5c1df4807281cdc5cbe45 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Mon, 6 Jan 2025 13:04:33 -0500 Subject: [PATCH 2/5] Only send over `logLevel` and `dependencyLogLevels` as flat initialization options --- crates/lsp/src/handlers_state.rs | 14 ++--- crates/lsp/src/main_loop.rs | 10 ---- crates/lsp/src/settings.rs | 42 +++------------ editors/code/package.json | 6 --- editors/code/src/common/README.md | 4 -- editors/code/src/common/constants.ts | 12 ----- editors/code/src/common/log/logging.ts | 29 ---------- editors/code/src/common/setup.ts | 19 ------- editors/code/src/common/vscodeapi.ts | 53 ------------------ editors/code/src/lsp.ts | 30 ++--------- editors/code/src/settings.ts | 74 ++++++-------------------- 11 files changed, 29 insertions(+), 264 deletions(-) delete mode 100644 editors/code/src/common/README.md delete mode 100644 editors/code/src/common/constants.ts delete mode 100644 editors/code/src/common/log/logging.ts delete mode 100644 editors/code/src/common/setup.ts delete mode 100644 editors/code/src/common/vscodeapi.ts diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs index 9a6c890f..8c76a356 100644 --- a/crates/lsp/src/handlers_state.rs +++ b/crates/lsp/src/handlers_state.rs @@ -67,9 +67,8 @@ pub(crate) fn initialize( log_tx: LogMessageSender, ) -> anyhow::Result { let InitializationOptions { - global_settings, - user_settings, - workspace_settings, + log_level, + dependency_log_levels, } = match params.initialization_options { Some(initialization_options) => InitializationOptions::from_value(initialization_options), None => InitializationOptions::default(), @@ -77,16 +76,11 @@ pub(crate) fn initialize( logging::init_logging( log_tx, - global_settings.log_level, - global_settings.dependency_log_levels, + log_level, + dependency_log_levels, params.client_info.as_ref(), ); - // TODO: Should these be "pulled" using the LSP server->client `configuration()` - // request instead? - lsp_state.user_client_settings = user_settings; - lsp_state.workspace_client_settings = workspace_settings; - // Defaults to UTF-16 let mut position_encoding = None; diff --git a/crates/lsp/src/main_loop.rs b/crates/lsp/src/main_loop.rs index 5bc48b3e..1218a7c9 100644 --- a/crates/lsp/src/main_loop.rs +++ b/crates/lsp/src/main_loop.rs @@ -26,8 +26,6 @@ use crate::handlers_state; use crate::handlers_state::ConsoleInputs; use crate::logging::LogMessageSender; use crate::logging::LogState; -use crate::settings::ClientSettings; -use crate::settings::ClientWorkspaceSettings; use crate::state::WorldState; use crate::tower_lsp::LspMessage; use crate::tower_lsp::LspNotification; @@ -150,12 +148,6 @@ pub(crate) struct GlobalState { /// Unlike `WorldState`, `LspState` cannot be cloned and is only accessed by /// exclusive handlers. pub(crate) struct LspState { - /// User level [`ClientSettings`] sent over from the client - pub(crate) user_client_settings: ClientSettings, - - /// Workspace level [`ClientSettings`] sent over from the client - pub(crate) workspace_client_settings: Vec, - /// The negociated encoding for document positions. Note that documents are /// always stored as UTF-8 in Rust Strings. This encoding is only used to /// translate UTF-16 positions sent by the client to UTF-8 ones. @@ -173,8 +165,6 @@ pub(crate) struct LspState { impl Default for LspState { fn default() -> Self { Self { - user_client_settings: ClientSettings::default(), - workspace_client_settings: Vec::new(), // Default encoding specified in the LSP protocol position_encoding: PositionEncoding::Wide(WideEncoding::Utf16), parsers: Default::default(), diff --git a/crates/lsp/src/settings.rs b/crates/lsp/src/settings.rs index 456cf6ba..a9b8569c 100644 --- a/crates/lsp/src/settings.rs +++ b/crates/lsp/src/settings.rs @@ -1,52 +1,24 @@ use serde::Deserialize; use serde_json::Value; -use url::Url; - -// These settings are only needed once, typically for initialization. -// They are read at the global scope on the client side and are never refreshed. -#[derive(Debug, Deserialize, Default, Clone)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub(crate) struct ClientGlobalSettings { - pub(crate) log_level: Option, - pub(crate) dependency_log_levels: Option, -} - -/// This is a direct representation of the user level settings schema sent -/// by the client. It is refreshed after configuration changes. -#[derive(Debug, Deserialize, Default, Clone)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub(crate) struct ClientSettings {} - -/// This is a direct representation of the workspace level settings schema sent by the -/// client. It is the same as the user level settings with the addition of the workspace -/// path. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub(crate) struct ClientWorkspaceSettings { - pub(crate) url: Url, - #[serde(flatten)] - pub(crate) settings: ClientSettings, -} /// This is the exact schema for initialization options sent in by the client -/// during initialization. +/// during initialization. Remember that initialization options are ones that are +/// strictly required at startup time, and most configuration options should really be +/// "pulled" dynamically by the server after startup and whenever we receive a +/// configuration change notification (#121). #[derive(Debug, Deserialize, Default)] #[cfg_attr(test, derive(PartialEq, Eq))] #[serde(rename_all = "camelCase")] pub(crate) struct InitializationOptions { - pub(crate) global_settings: ClientGlobalSettings, - pub(crate) user_settings: ClientSettings, - pub(crate) workspace_settings: Vec, + pub(crate) log_level: Option, + pub(crate) dependency_log_levels: Option, } impl InitializationOptions { pub(crate) fn from_value(value: Value) -> Self { serde_json::from_value(value) .map_err(|err| { - tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings."); + tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default settings."); }) .unwrap_or_default() } diff --git a/editors/code/package.json b/editors/code/package.json index 16ffdfa9..582b5e17 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -6,10 +6,6 @@ "publisher": "Posit", "license": "MIT", "repository": "https://github.com/posit-dev/air", - "serverInfo": { - "name": "Air", - "module": "air" - }, "engines": { "vscode": "^1.90.0" }, @@ -115,12 +111,10 @@ "@types/p-queue": "^3.1.0", "p-queue": "npm:@esm2cjs/p-queue@^7.3.0", "adm-zip": "^0.5.16", - "fs-extra": "^11.2.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { "@types/adm-zip": "^0.5.6", - "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.9", "@types/node": "20.x", "@types/vscode": "^1.90.0", diff --git a/editors/code/src/common/README.md b/editors/code/src/common/README.md deleted file mode 100644 index 31da5b8a..00000000 --- a/editors/code/src/common/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This directory contains "common" utilities used across language server extensions. - -They are pulled directly from this MIT licensed repo: -https://github.com/microsoft/vscode-python-tools-extension-template diff --git a/editors/code/src/common/constants.ts b/editors/code/src/common/constants.ts deleted file mode 100644 index 11fffe6f..00000000 --- a/editors/code/src/common/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// https://github.com/microsoft/vscode-python-tools-extension-template - -import * as path from "path"; - -const folderName = path.basename(__dirname); - -export const EXTENSION_ROOT_DIR = - folderName === "common" - ? path.dirname(path.dirname(__dirname)) - : path.dirname(__dirname); diff --git a/editors/code/src/common/log/logging.ts b/editors/code/src/common/log/logging.ts deleted file mode 100644 index 45f62f23..00000000 --- a/editors/code/src/common/log/logging.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// https://github.com/microsoft/vscode-python-tools-extension-template - -import * as util from "util"; -import { Disposable, OutputChannel } from "vscode"; - -type Arguments = unknown[]; -class OutputChannelLogger { - constructor(private readonly channel: OutputChannel) {} - - public traceLog(...data: Arguments): void { - this.channel.appendLine(util.format(...data)); - } -} - -let channel: OutputChannelLogger | undefined; -export function registerLogger(outputChannel: OutputChannel): Disposable { - channel = new OutputChannelLogger(outputChannel); - return { - dispose: () => { - channel = undefined; - }, - }; -} - -export function traceLog(...args: Arguments): void { - channel?.traceLog(...args); -} diff --git a/editors/code/src/common/setup.ts b/editors/code/src/common/setup.ts deleted file mode 100644 index abd33a6d..00000000 --- a/editors/code/src/common/setup.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// https://github.com/microsoft/vscode-python-tools-extension-template - -import * as path from "path"; -import * as fs from "fs-extra"; -import { EXTENSION_ROOT_DIR } from "./constants"; - -export interface IServerInfo { - name: string; - module: string; -} - -export function loadServerDefaults(): IServerInfo { - const packageJson = path.join(EXTENSION_ROOT_DIR, "package.json"); - const content = fs.readFileSync(packageJson).toString(); - const config = JSON.parse(content); - return config.serverInfo as IServerInfo; -} diff --git a/editors/code/src/common/vscodeapi.ts b/editors/code/src/common/vscodeapi.ts deleted file mode 100644 index e43f109f..00000000 --- a/editors/code/src/common/vscodeapi.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// https://github.com/microsoft/vscode-python-tools-extension-template - -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - commands, - ConfigurationScope, - Disposable, - LogOutputChannel, - Uri, - window, - workspace, - WorkspaceConfiguration, - WorkspaceFolder, -} from "vscode"; - -export function createOutputChannel(name: string): LogOutputChannel { - return window.createOutputChannel(name, { log: true }); -} - -export function getConfiguration( - config: string, - scope?: ConfigurationScope -): WorkspaceConfiguration { - return workspace.getConfiguration(config, scope); -} - -export function registerCommand( - command: string, - callback: (...args: any[]) => any, - thisArg?: any -): Disposable { - return commands.registerCommand(command, callback, thisArg); -} - -export const { onDidChangeConfiguration } = workspace; - -export function isVirtualWorkspace(): boolean { - const isVirtual = - workspace.workspaceFolders && - workspace.workspaceFolders.every((f) => f.uri.scheme !== "file"); - return !!isVirtual; -} - -export function getWorkspaceFolders(): readonly WorkspaceFolder[] { - return workspace.workspaceFolders ?? []; -} - -export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { - return workspace.getWorkspaceFolder(uri); -} diff --git a/editors/code/src/lsp.ts b/editors/code/src/lsp.ts index 5d691f05..3426b992 100644 --- a/editors/code/src/lsp.ts +++ b/editors/code/src/lsp.ts @@ -1,14 +1,7 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import { default as PQueue } from "p-queue"; -import { IServerInfo, loadServerDefaults } from "./common/setup"; -import { registerLogger, traceLog } from "./common/log/logging"; -import { - getGlobalSettings, - getUserSettings, - getWorkspaceSettings, - IInitializationOptions, -} from "./settings"; +import { getInitializationOptions } from "./settings"; // All session management operations are put on a queue. They can't run // concurrently and either result in a started or stopped state. Starting when @@ -22,8 +15,6 @@ enum State { export class Lsp { public client: lc.LanguageClient | null = null; - private serverInfo: IServerInfo; - // We use the same output channel for all LSP instances (e.g. a new instance // after a restart) to avoid having multiple channels in the Output viewpane. private channel: vscode.OutputChannel; @@ -33,8 +24,7 @@ export class Lsp { constructor(context: vscode.ExtensionContext) { this.channel = vscode.window.createOutputChannel("Air Language Server"); - context.subscriptions.push(this.channel, registerLogger(this.channel)); - this.serverInfo = loadServerDefaults(); + context.subscriptions.push(this.channel); this.stateQueue = new PQueue({ concurrency: 1 }); } @@ -63,21 +53,7 @@ export class Lsp { return; } - // Log server information - traceLog(`Name: ${this.serverInfo.name}`); - traceLog(`Module: ${this.serverInfo.module}`); - - const globalSettings = await getGlobalSettings(this.serverInfo.module); - const userSettings = await getUserSettings(this.serverInfo.module); - const workspaceSettings = await getWorkspaceSettings( - this.serverInfo.module - ); - - const initializationOptions: IInitializationOptions = { - globalSettings, - userSettings, - workspaceSettings, - }; + const initializationOptions = await getInitializationOptions("air"); let serverOptions: lc.ServerOptions = { command: "air", diff --git a/editors/code/src/settings.ts b/editors/code/src/settings.ts index bfe4692f..cd1b888e 100644 --- a/editors/code/src/settings.ts +++ b/editors/code/src/settings.ts @@ -1,42 +1,19 @@ -import { WorkspaceConfiguration, WorkspaceFolder } from "vscode"; -import { getConfiguration, getWorkspaceFolders } from "./common/vscodeapi"; +import { ConfigurationScope, workspace, WorkspaceConfiguration } from "vscode"; type LogLevel = "error" | "warn" | "info" | "debug" | "trace"; -// One time settings that aren't ever refreshed within the extension's lifetime. -// They are read at the user (i.e. global) scope. -export interface IGlobalSettings { - logLevel?: LogLevel; - dependencyLogLevels?: string; -} - -// Client representation of user level client settings. -// TODO: These are refreshed using a `Configuration` LSP request from the server. -// (It is possible we should ONLY get these through `Configuration` and not through -// initializationOptions) -export interface ISettings {} - -// Client representation of workspace level client settings. -// Same as the user level settings, with the addition of the workspace path. -// TODO: These are refreshed using a `Configuration` LSP request from the server. -// (It is possible we should ONLY get these through `Configuration` and not through -// initializationOptions) -export interface IWorkspaceSettings { - url: string; - settings: ISettings; -} - // This is a direct representation of the Client settings sent to the Server in the -// `initializationOptions` field of `InitializeParams` +// `initializationOptions` field of `InitializeParams`. These are only pulled at the +// user level since they are global settings on the server side (and are scoped to +// `"scope": "application"` in `package.json` so they can't even be set at workspace level). export type IInitializationOptions = { - globalSettings: IGlobalSettings; - userSettings: ISettings; - workspaceSettings: IWorkspaceSettings[]; + logLevel?: LogLevel; + dependencyLogLevels?: string; }; -export async function getGlobalSettings( +export async function getInitializationOptions( namespace: string -): Promise { +): Promise { const config = getConfiguration(namespace); return { @@ -48,34 +25,6 @@ export async function getGlobalSettings( }; } -export async function getUserSettings(namespace: string): Promise { - const config = getConfiguration(namespace); - - return {}; -} - -export function getWorkspaceSettings( - namespace: string -): Promise { - return Promise.all( - getWorkspaceFolders().map((workspaceFolder) => - getWorkspaceFolderSettings(namespace, workspaceFolder) - ) - ); -} - -async function getWorkspaceFolderSettings( - namespace: string, - workspace: WorkspaceFolder -): Promise { - const config = getConfiguration(namespace, workspace.uri); - - return { - url: workspace.uri.toString(), - settings: {}, - }; -} - function getOptionalUserValue( config: WorkspaceConfiguration, key: string @@ -83,3 +32,10 @@ function getOptionalUserValue( const inspect = config.inspect(key); return inspect?.globalValue; } + +function getConfiguration( + config: string, + scope?: ConfigurationScope +): WorkspaceConfiguration { + return workspace.getConfiguration(config, scope); +} From 36d272626c90af4a8c6ed71c52c2d49d8a0ab63a Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Mon, 6 Jan 2025 13:06:04 -0500 Subject: [PATCH 3/5] Remove unused cfg-attr --- crates/lsp/src/settings.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/lsp/src/settings.rs b/crates/lsp/src/settings.rs index a9b8569c..1f338209 100644 --- a/crates/lsp/src/settings.rs +++ b/crates/lsp/src/settings.rs @@ -7,7 +7,6 @@ use serde_json::Value; /// "pulled" dynamically by the server after startup and whenever we receive a /// configuration change notification (#121). #[derive(Debug, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] #[serde(rename_all = "camelCase")] pub(crate) struct InitializationOptions { pub(crate) log_level: Option, From 5fd3ba48780f0276d2325c982bfeb1acbf2e8afa Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Tue, 7 Jan 2025 10:25:13 -0500 Subject: [PATCH 4/5] Simplify `InitializationOptions` creation --- crates/lsp/src/handlers_state.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs index 8c76a356..faca7987 100644 --- a/crates/lsp/src/handlers_state.rs +++ b/crates/lsp/src/handlers_state.rs @@ -69,10 +69,10 @@ pub(crate) fn initialize( let InitializationOptions { log_level, dependency_log_levels, - } = match params.initialization_options { - Some(initialization_options) => InitializationOptions::from_value(initialization_options), - None => InitializationOptions::default(), - }; + } = params.initialization_options.map_or_else( + InitializationOptions::default, + InitializationOptions::from_value, + ); logging::init_logging( log_tx, From d54d543cca10adbdd65428e390dac4d98a2ee919 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Tue, 7 Jan 2025 10:27:33 -0500 Subject: [PATCH 5/5] No need for `async`, and don't use interfacing naming --- editors/code/src/lsp.ts | 2 +- editors/code/src/settings.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/editors/code/src/lsp.ts b/editors/code/src/lsp.ts index 3426b992..d0c7c18b 100644 --- a/editors/code/src/lsp.ts +++ b/editors/code/src/lsp.ts @@ -53,7 +53,7 @@ export class Lsp { return; } - const initializationOptions = await getInitializationOptions("air"); + const initializationOptions = getInitializationOptions("air"); let serverOptions: lc.ServerOptions = { command: "air", diff --git a/editors/code/src/settings.ts b/editors/code/src/settings.ts index cd1b888e..e6c95c04 100644 --- a/editors/code/src/settings.ts +++ b/editors/code/src/settings.ts @@ -6,14 +6,14 @@ type LogLevel = "error" | "warn" | "info" | "debug" | "trace"; // `initializationOptions` field of `InitializeParams`. These are only pulled at the // user level since they are global settings on the server side (and are scoped to // `"scope": "application"` in `package.json` so they can't even be set at workspace level). -export type IInitializationOptions = { +export type InitializationOptions = { logLevel?: LogLevel; dependencyLogLevels?: string; }; -export async function getInitializationOptions( +export function getInitializationOptions( namespace: string -): Promise { +): InitializationOptions { const config = getConfiguration(namespace); return {