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..faca7987 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,9 +66,13 @@ 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 { + log_level, + dependency_log_levels, + } = params.initialization_options.map_or_else( + InitializationOptions::default, + InitializationOptions::from_value, + ); logging::init_logging( log_tx, 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..1218a7c9 100644 --- a/crates/lsp/src/main_loop.rs +++ b/crates/lsp/src/main_loop.rs @@ -145,7 +145,7 @@ 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 { /// The negociated encoding for document positions. Note that documents are diff --git a/crates/lsp/src/settings.rs b/crates/lsp/src/settings.rs new file mode 100644 index 00000000..1f338209 --- /dev/null +++ b/crates/lsp/src/settings.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; +use serde_json::Value; + +/// This is the exact schema for initialization options sent in by the client +/// 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)] +#[serde(rename_all = "camelCase")] +pub(crate) struct InitializationOptions { + 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 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..582b5e17 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -35,6 +35,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" diff --git a/editors/code/src/lsp.ts b/editors/code/src/lsp.ts index f37fc43c..d0c7c18b 100644 --- a/editors/code/src/lsp.ts +++ b/editors/code/src/lsp.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import { default as PQueue } from "p-queue"; +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 @@ -52,7 +53,9 @@ export class Lsp { return; } - let options: lc.ServerOptions = { + const initializationOptions = getInitializationOptions("air"); + + let serverOptions: lc.ServerOptions = { command: "air", args: ["language-server"], }; @@ -71,13 +74,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..e6c95c04 --- /dev/null +++ b/editors/code/src/settings.ts @@ -0,0 +1,41 @@ +import { ConfigurationScope, workspace, WorkspaceConfiguration } from "vscode"; + +type LogLevel = "error" | "warn" | "info" | "debug" | "trace"; + +// This is a direct representation of the Client settings sent to the Server in the +// `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 InitializationOptions = { + logLevel?: LogLevel; + dependencyLogLevels?: string; +}; + +export function getInitializationOptions( + namespace: string +): InitializationOptions { + const config = getConfiguration(namespace); + + return { + logLevel: getOptionalUserValue(config, "logLevel"), + dependencyLogLevels: getOptionalUserValue( + config, + "dependencyLogLevels" + ), + }; +} + +function getOptionalUserValue( + config: WorkspaceConfiguration, + key: string +): T | undefined { + const inspect = config.inspect(key); + return inspect?.globalValue; +} + +function getConfiguration( + config: string, + scope?: ConfigurationScope +): WorkspaceConfiguration { + return workspace.getConfiguration(config, scope); +}