Skip to content

Commit

Permalink
Send structured InitializationOptions to the server
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DavisVaughan committed Dec 20, 2024
1 parent 5446d7e commit cb98410
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 16 additions & 5 deletions crates/lsp/src/handlers_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -65,17 +66,27 @@ pub(crate) fn initialize(
state: &mut WorldState,
log_tx: LogMessageSender,
) -> anyhow::Result<InitializeResult> {
// 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;

Expand Down
1 change: 1 addition & 0 deletions crates/lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion crates/lsp/src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -145,9 +147,15 @@ pub(crate) struct GlobalState {
log_tx: Option<LogMessageSender>,
}

/// 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<ClientWorkspaceSettings>,

/// 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.
Expand All @@ -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(),
Expand Down
53 changes: 53 additions & 0 deletions crates/lsp/src/settings.rs
Original file line number Diff line number Diff line change
@@ -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<crate::logging::LogLevel>,
pub(crate) dependency_log_levels: Option<String>,
}

/// 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<ClientWorkspaceSettings>,
}

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()
}
}
59 changes: 58 additions & 1 deletion editors/code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions editors/code/src/common/README.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions editors/code/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -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);
29 changes: 29 additions & 0 deletions editors/code/src/common/log/logging.ts
Original file line number Diff line number Diff line change
@@ -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);
}
19 changes: 19 additions & 0 deletions editors/code/src/common/setup.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit cb98410

Please sign in to comment.