Skip to content

Commit 92887d3

Browse files
authored
Send structured InitializationOptions to the server (#121)
* 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 * Only send over `logLevel` and `dependencyLogLevels` as flat initialization options * Remove unused cfg-attr * Simplify `InitializationOptions` creation * No need for `async`, and don't use interfacing naming
1 parent 5446d7e commit 92887d3

File tree

9 files changed

+167
-8
lines changed

9 files changed

+167
-8
lines changed

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"editor.formatOnSaveMode": "file",
55
"editor.defaultFormatter": "rust-lang.rust-analyzer"
66
},
7+
"[typescript]": {
8+
"editor.formatOnSave": true,
9+
"editor.defaultFormatter": "esbenp.prettier-vscode"
10+
},
711
"rust-analyzer.check.command": "clippy",
812
"rust-analyzer.imports.prefix": "crate",
913
"rust-analyzer.imports.granularity.group": "item",

crates/lsp/src/handlers_state.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::documents::Document;
3636
use crate::logging;
3737
use crate::logging::LogMessageSender;
3838
use crate::main_loop::LspState;
39+
use crate::settings::InitializationOptions;
3940
use crate::state::workspace_uris;
4041
use crate::state::WorldState;
4142

@@ -65,9 +66,13 @@ pub(crate) fn initialize(
6566
state: &mut WorldState,
6667
log_tx: LogMessageSender,
6768
) -> anyhow::Result<InitializeResult> {
68-
// TODO: Get user specified options from `params.initialization_options`
69-
let log_level = None;
70-
let dependency_log_levels = None;
69+
let InitializationOptions {
70+
log_level,
71+
dependency_log_levels,
72+
} = params.initialization_options.map_or_else(
73+
InitializationOptions::default,
74+
InitializationOptions::from_value,
75+
);
7176

7277
logging::init_logging(
7378
log_tx,

crates/lsp/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod handlers_state;
1515
pub mod logging;
1616
pub mod main_loop;
1717
pub mod rust_analyzer;
18+
pub mod settings;
1819
pub mod state;
1920
pub mod to_proto;
2021
pub mod tower_lsp;

crates/lsp/src/main_loop.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ pub(crate) struct GlobalState {
145145
log_tx: Option<LogMessageSender>,
146146
}
147147

148-
/// Unlike `WorldState`, `ParserState` cannot be cloned and is only accessed by
148+
/// Unlike `WorldState`, `LspState` cannot be cloned and is only accessed by
149149
/// exclusive handlers.
150150
pub(crate) struct LspState {
151151
/// The negociated encoding for document positions. Note that documents are

crates/lsp/src/settings.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use serde::Deserialize;
2+
use serde_json::Value;
3+
4+
/// This is the exact schema for initialization options sent in by the client
5+
/// during initialization. Remember that initialization options are ones that are
6+
/// strictly required at startup time, and most configuration options should really be
7+
/// "pulled" dynamically by the server after startup and whenever we receive a
8+
/// configuration change notification (#121).
9+
#[derive(Debug, Deserialize, Default)]
10+
#[serde(rename_all = "camelCase")]
11+
pub(crate) struct InitializationOptions {
12+
pub(crate) log_level: Option<crate::logging::LogLevel>,
13+
pub(crate) dependency_log_levels: Option<String>,
14+
}
15+
16+
impl InitializationOptions {
17+
pub(crate) fn from_value(value: Value) -> Self {
18+
serde_json::from_value(value)
19+
.map_err(|err| {
20+
tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default settings.");
21+
})
22+
.unwrap_or_default()
23+
}
24+
}

editors/code/package-lock.json

Lines changed: 58 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editors/code/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,29 @@
3535
]
3636
}
3737
],
38+
"configuration": {
39+
"properties": {
40+
"air.logLevel": {
41+
"default": null,
42+
"markdownDescription": "Controls the log level of the language server.\n\n**This setting requires a restart to take effect.**",
43+
"enum": [
44+
"error",
45+
"warning",
46+
"info",
47+
"debug",
48+
"trace"
49+
],
50+
"scope": "application",
51+
"type": "string"
52+
},
53+
"air.dependencyLogLevels": {
54+
"default": null,
55+
"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.**",
56+
"scope": "application",
57+
"type": "string"
58+
}
59+
}
60+
},
3861
"configurationDefaults": {
3962
"[r]": {
4063
"editor.defaultFormatter": "Posit.air"

editors/code/src/lsp.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode";
22
import * as lc from "vscode-languageclient/node";
33
import { default as PQueue } from "p-queue";
4+
import { getInitializationOptions } from "./settings";
45

56
// All session management operations are put on a queue. They can't run
67
// concurrently and either result in a started or stopped state. Starting when
@@ -52,7 +53,9 @@ export class Lsp {
5253
return;
5354
}
5455

55-
let options: lc.ServerOptions = {
56+
const initializationOptions = getInitializationOptions("air");
57+
58+
let serverOptions: lc.ServerOptions = {
5659
command: "air",
5760
args: ["language-server"],
5861
};
@@ -71,13 +74,14 @@ export class Lsp {
7174
vscode.workspace.createFileSystemWatcher("**/*.[Rr]"),
7275
},
7376
outputChannel: this.channel,
77+
initializationOptions: initializationOptions,
7478
};
7579

7680
const client = new lc.LanguageClient(
7781
"airLanguageServer",
7882
"Air Language Server",
79-
options,
80-
clientOptions,
83+
serverOptions,
84+
clientOptions
8185
);
8286
await client.start();
8387

editors/code/src/settings.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ConfigurationScope, workspace, WorkspaceConfiguration } from "vscode";
2+
3+
type LogLevel = "error" | "warn" | "info" | "debug" | "trace";
4+
5+
// This is a direct representation of the Client settings sent to the Server in the
6+
// `initializationOptions` field of `InitializeParams`. These are only pulled at the
7+
// user level since they are global settings on the server side (and are scoped to
8+
// `"scope": "application"` in `package.json` so they can't even be set at workspace level).
9+
export type InitializationOptions = {
10+
logLevel?: LogLevel;
11+
dependencyLogLevels?: string;
12+
};
13+
14+
export function getInitializationOptions(
15+
namespace: string
16+
): InitializationOptions {
17+
const config = getConfiguration(namespace);
18+
19+
return {
20+
logLevel: getOptionalUserValue<LogLevel>(config, "logLevel"),
21+
dependencyLogLevels: getOptionalUserValue<string>(
22+
config,
23+
"dependencyLogLevels"
24+
),
25+
};
26+
}
27+
28+
function getOptionalUserValue<T>(
29+
config: WorkspaceConfiguration,
30+
key: string
31+
): T | undefined {
32+
const inspect = config.inspect<T>(key);
33+
return inspect?.globalValue;
34+
}
35+
36+
function getConfiguration(
37+
config: string,
38+
scope?: ConfigurationScope
39+
): WorkspaceConfiguration {
40+
return workspace.getConfiguration(config, scope);
41+
}

0 commit comments

Comments
 (0)