mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
Implement client setting initialization and resolution for ruff server
(#10764)
## Summary When a language server initializes, it is passed a serialized JSON object, which is known as its "initialization options". Until now, `ruff server` has ignored those initialization options, meaning that user-provided settings haven't worked. This PR is the first step for supporting settings from the LSP client. It implements procedures to deserialize initialization options into a settings object, and then resolve those settings objects into concrete settings for each workspace. One of the goals for user settings implementation in `ruff server` is backwards compatibility with `ruff-lsp`'s settings. We won't support all settings that `ruff-lsp` had, but the ones that we do support should work the same and use the same schema as `ruff-lsp`. These are the existing settings from `ruff-lsp` that we will continue to support, and which are part of the settings schema in this PR: | Setting | Default Value | Description | |----------------------------------------|---------------|----------------------------------------------------------------------------------------| | `codeAction.disableRuleComment.enable` | `true` | Whether to display Quick Fix actions to disable rules via `noqa` suppression comments. | | `codeAction.fixViolation.enable` | `true` | Whether to display Quick Fix actions to autofix violations. | | `fixAll` | `true` | Whether to register Ruff as capable of handling `source.fixAll` actions. | | `lint.enable` | `true` | Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter. | | `organizeImports` | `true` | Whether to register Ruff as capable of handling `source.organizeImports` actions. | To be clear: this PR does not implement 'support' for these settings, individually. Rather, it constructs a framework for these settings to be used by the server in the future. Notably, we are choosing *not* to support `lint.args` and `format.args` as settings for `ruff server`. This is because we're now interfacing with Ruff at a lower level than its CLI, and converting CLI arguments back into configuration is too involved. We will have support for linter and formatter specific settings in follow-up PRs. We will also 'hook up' user settings to work with the server in follow up PRs. ## Test Plan ### Snapshot Tests Tests have been created in `crates/ruff_server/src/session/settings/tests.rs` to ensure that deserialization and settings resolution works as expected. ### Manual Testing Since we aren't using the resolved settings anywhere yet, we'll have to add a few printing statements. We want to capture what the resolved settings look like when sent as part of a snapshot, so modify `Session::take_snapshot` to be the following: ```rust pub(crate) fn take_snapshot(&self, url: &Url) -> Option<DocumentSnapshot> { let resolved_settings = self.workspaces.client_settings(url, &self.global_settings); tracing::info!("Resolved settings for document {url}: {resolved_settings:?}"); Some(DocumentSnapshot { configuration: self.workspaces.configuration(url)?.clone(), resolved_client_capabilities: self.resolved_client_capabilities.clone(), client_settings: resolved_settings, document_ref: self.workspaces.snapshot(url)?, position_encoding: self.position_encoding, url: url.clone(), }) } ``` Once you've done that, build the server and start up your extension testing environment. 1. Set up a workspace in VS Code with two workspace folders, each one having some variant of Ruff file-based configuration (`pyproject.toml`, `ruff.toml`, etc.). We'll call these folders `folder_a` and `folder_b`. 2. In each folder, open up `.vscode/settings.json`. 3. In folder A, use these settings: ```json { "ruff.codeAction.disableRuleComment": { "enable": true } } ``` 4. In folder B, use these settings: ```json { "ruff.codeAction.disableRuleComment": { "enable": false } } ``` 5. Finally, open up your VS Code User Settings and un-check the `Ruff > Code Action: Disable Rule Comment` setting. 6. When opening files in `folder_a`, you should see logs that look like this: ``` Resolved settings for document <file>: ResolvedClientSettings { fix_all: true, organize_imports: true, lint_enable: true, disable_rule_comment_enable: true, fix_violation_enable: true } ``` 7. When opening files in `folder_b`, you should see logs that look like this: ``` Resolved settings for document <file>: ResolvedClientSettings { fix_all: true, organize_imports: true, lint_enable: true, disable_rule_comment_enable: false, fix_violation_enable: true } ``` 8. To test invalid configuration, change `.vscode/settings.json` in either folder to be this: ```json { "ruff.codeAction.disableRuleComment": { "enable": "invalid" }, } ``` 10. You should now see these error logs: ``` <time> [info] <duration> ERROR ruff_server::session::settings Failed to deserialize initialization options: data did not match any variant of untagged enum InitializationOptions. Falling back to default client settings... <time> [info] <duration> WARN ruff_server::server No workspace settings found for file:///Users/jane/testbed/pandas <duration> WARN ruff_server::server No workspace settings found for file:///Users/jane/foss/scipy ``` 11. Opening files in either folder should now print the following configuration: ``` Resolved settings for document <file>: ResolvedClientSettings { fix_all: true, organize_imports: true, lint_enable: true, disable_rule_comment_enable: true, fix_violation_enable: true } ```
This commit is contained in:
parent
a4ee9c1978
commit
a184dc68f5
7 changed files with 677 additions and 39 deletions
1
crates/ruff_server/resources/test/fixtures/settings/empty.json
vendored
Normal file
1
crates/ruff_server/resources/test/fixtures/settings/empty.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
14
crates/ruff_server/resources/test/fixtures/settings/global_only.json
vendored
Normal file
14
crates/ruff_server/resources/test/fixtures/settings/global_only.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"settings": {
|
||||
"codeAction": {
|
||||
"disableRuleComment": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"run": "onSave"
|
||||
},
|
||||
"fixAll": false,
|
||||
"logLevel": "warn"
|
||||
}
|
||||
}
|
101
crates/ruff_server/resources/test/fixtures/settings/vs_code_initialization_options.json
vendored
Normal file
101
crates/ruff_server/resources/test/fixtures/settings/vs_code_initialization_options.json
vendored
Normal file
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"settings": [
|
||||
{
|
||||
"experimentalServer": true,
|
||||
"cwd": "/Users/test/projects/pandas",
|
||||
"workspace": "file:///Users/test/projects/pandas",
|
||||
"path": [],
|
||||
"ignoreStandardLibrary": true,
|
||||
"interpreter": [
|
||||
"/Users/test/projects/pandas/.venv/bin/python"
|
||||
],
|
||||
"importStrategy": "fromEnvironment",
|
||||
"codeAction": {
|
||||
"fixViolation": {
|
||||
"enable": false
|
||||
},
|
||||
"disableRuleComment": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"enable": true,
|
||||
"run": "onType",
|
||||
"args": [
|
||||
"--preview"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"args": []
|
||||
},
|
||||
"enable": true,
|
||||
"organizeImports": true,
|
||||
"fixAll": true,
|
||||
"showNotifications": "off"
|
||||
},
|
||||
{
|
||||
"experimentalServer": true,
|
||||
"cwd": "/Users/test/projects/scipy",
|
||||
"workspace": "file:///Users/test/projects/scipy",
|
||||
"path": [],
|
||||
"ignoreStandardLibrary": true,
|
||||
"interpreter": [
|
||||
"/Users/test/projects/scipy/.venv/bin/python"
|
||||
],
|
||||
"importStrategy": "fromEnvironment",
|
||||
"codeAction": {
|
||||
"fixViolation": {
|
||||
"enable": false
|
||||
},
|
||||
"disableRuleComment": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"enable": true,
|
||||
"run": "onType",
|
||||
"args": [
|
||||
"--preview"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"args": []
|
||||
},
|
||||
"enable": true,
|
||||
"organizeImports": true,
|
||||
"fixAll": true,
|
||||
"showNotifications": "off"
|
||||
}
|
||||
],
|
||||
"globalSettings": {
|
||||
"experimentalServer": true,
|
||||
"cwd": "/",
|
||||
"workspace": "/",
|
||||
"path": [],
|
||||
"ignoreStandardLibrary": true,
|
||||
"interpreter": [],
|
||||
"importStrategy": "fromEnvironment",
|
||||
"codeAction": {
|
||||
"fixViolation": {
|
||||
"enable": false
|
||||
},
|
||||
"disableRuleComment": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"enable": true,
|
||||
"run": "onType",
|
||||
"args": [
|
||||
"--preview"
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"args": []
|
||||
},
|
||||
"enable": true,
|
||||
"organizeImports": true,
|
||||
"fixAll": false,
|
||||
"showNotifications": "off"
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ use types::WorkspaceFoldersServerCapabilities;
|
|||
use self::schedule::event_loop_thread;
|
||||
use self::schedule::Scheduler;
|
||||
use self::schedule::Task;
|
||||
use crate::session::AllSettings;
|
||||
use crate::session::ClientSettings;
|
||||
use crate::session::Session;
|
||||
use crate::PositionEncoding;
|
||||
|
||||
|
@ -47,14 +49,34 @@ impl Server {
|
|||
let init_params: types::InitializeParams = serde_json::from_value(params)?;
|
||||
|
||||
let client_capabilities = init_params.capabilities;
|
||||
let server_capabilities = Self::server_capabilities(&client_capabilities);
|
||||
let position_encoding = Self::find_best_position_encoding(&client_capabilities);
|
||||
let server_capabilities = Self::server_capabilities(position_encoding);
|
||||
|
||||
let AllSettings {
|
||||
global_settings,
|
||||
mut workspace_settings,
|
||||
} = AllSettings::from_value(init_params.initialization_options.unwrap_or_default());
|
||||
|
||||
let mut workspace_for_uri = |uri| {
|
||||
let Some(workspace_settings) = workspace_settings.as_mut() else {
|
||||
return (uri, ClientSettings::default());
|
||||
};
|
||||
let settings = workspace_settings.remove(&uri).unwrap_or_else(|| {
|
||||
tracing::warn!("No workspace settings found for {uri}");
|
||||
ClientSettings::default()
|
||||
});
|
||||
(uri, settings)
|
||||
};
|
||||
|
||||
let workspaces = init_params
|
||||
.workspace_folders
|
||||
.map(|folders| folders.into_iter().map(|folder| folder.uri).collect())
|
||||
.map(|folders| folders.into_iter().map(|folder| {
|
||||
workspace_for_uri(folder.uri)
|
||||
}).collect())
|
||||
.or_else(|| {
|
||||
tracing::debug!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
|
||||
Some(vec![types::Url::from_file_path(std::env::current_dir().ok()?).ok()?])
|
||||
let uri = types::Url::from_file_path(std::env::current_dir().ok()?).ok()?;
|
||||
Some(vec![workspace_for_uri(uri)])
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.")
|
||||
|
@ -74,7 +96,12 @@ impl Server {
|
|||
conn,
|
||||
threads,
|
||||
worker_threads,
|
||||
session: Session::new(&client_capabilities, &server_capabilities, &workspaces)?,
|
||||
session: Session::new(
|
||||
&client_capabilities,
|
||||
position_encoding,
|
||||
global_settings,
|
||||
workspaces,
|
||||
)?,
|
||||
client_capabilities,
|
||||
})
|
||||
}
|
||||
|
@ -176,8 +203,8 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
fn server_capabilities(client_capabilities: &ClientCapabilities) -> types::ServerCapabilities {
|
||||
let position_encoding = client_capabilities
|
||||
fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding {
|
||||
client_capabilities
|
||||
.general
|
||||
.as_ref()
|
||||
.and_then(|general_capabilities| general_capabilities.position_encodings.as_ref())
|
||||
|
@ -187,7 +214,10 @@ impl Server {
|
|||
.filter_map(|encoding| PositionEncoding::try_from(encoding).ok())
|
||||
.max() // this selects the highest priority position encoding
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities {
|
||||
types::ServerCapabilities {
|
||||
position_encoding: Some(position_encoding.into()),
|
||||
code_action_provider: Some(types::CodeActionProviderCapability::Options(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Data model, state management, and configuration resolution.
|
||||
|
||||
mod capabilities;
|
||||
mod settings;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
@ -7,14 +8,16 @@ use std::path::{Path, PathBuf};
|
|||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use lsp_types::{ClientCapabilities, ServerCapabilities, Url};
|
||||
use lsp_types::{ClientCapabilities, Url};
|
||||
use ruff_workspace::resolver::{ConfigurationTransformer, Relativity};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::edit::{Document, DocumentVersion};
|
||||
use crate::PositionEncoding;
|
||||
|
||||
use self::settings::ResolvedClientCapabilities;
|
||||
use self::capabilities::ResolvedClientCapabilities;
|
||||
use self::settings::ResolvedClientSettings;
|
||||
pub(crate) use self::settings::{AllSettings, ClientSettings};
|
||||
|
||||
/// The global state for the LSP
|
||||
pub(crate) struct Session {
|
||||
|
@ -22,6 +25,8 @@ pub(crate) struct Session {
|
|||
workspaces: Workspaces,
|
||||
/// The global position encoding, negotiated during LSP initialization.
|
||||
position_encoding: PositionEncoding,
|
||||
/// Global settings provided by the client.
|
||||
global_settings: ClientSettings,
|
||||
/// Tracks what LSP features the client supports and doesn't support.
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
}
|
||||
|
@ -31,6 +36,8 @@ pub(crate) struct Session {
|
|||
pub(crate) struct DocumentSnapshot {
|
||||
configuration: Arc<RuffConfiguration>,
|
||||
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||
#[allow(dead_code)]
|
||||
client_settings: settings::ResolvedClientSettings,
|
||||
document_ref: DocumentRef,
|
||||
position_encoding: PositionEncoding,
|
||||
url: Url,
|
||||
|
@ -50,6 +57,7 @@ pub(crate) struct Workspaces(BTreeMap<PathBuf, Workspace>);
|
|||
pub(crate) struct Workspace {
|
||||
open_documents: OpenDocuments,
|
||||
configuration: Arc<RuffConfiguration>,
|
||||
settings: ClientSettings,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -73,15 +81,13 @@ pub(crate) struct DocumentRef {
|
|||
impl Session {
|
||||
pub(crate) fn new(
|
||||
client_capabilities: &ClientCapabilities,
|
||||
server_capabilities: &ServerCapabilities,
|
||||
workspaces: &[Url],
|
||||
position_encoding: PositionEncoding,
|
||||
global_settings: ClientSettings,
|
||||
workspaces: Vec<(Url, ClientSettings)>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
position_encoding: server_capabilities
|
||||
.position_encoding
|
||||
.as_ref()
|
||||
.and_then(|encoding| encoding.try_into().ok())
|
||||
.unwrap_or_default(),
|
||||
position_encoding,
|
||||
global_settings,
|
||||
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||
client_capabilities,
|
||||
)),
|
||||
|
@ -90,9 +96,12 @@ impl Session {
|
|||
}
|
||||
|
||||
pub(crate) fn take_snapshot(&self, url: &Url) -> Option<DocumentSnapshot> {
|
||||
let resolved_settings = self.workspaces.client_settings(url, &self.global_settings);
|
||||
tracing::info!("Resolved settings for document {url}: {resolved_settings:?}");
|
||||
Some(DocumentSnapshot {
|
||||
configuration: self.workspaces.configuration(url)?.clone(),
|
||||
resolved_client_capabilities: self.resolved_client_capabilities.clone(),
|
||||
client_settings: resolved_settings,
|
||||
document_ref: self.workspaces.snapshot(url)?,
|
||||
position_encoding: self.position_encoding,
|
||||
url: url.clone(),
|
||||
|
@ -220,16 +229,18 @@ impl DocumentSnapshot {
|
|||
}
|
||||
|
||||
impl Workspaces {
|
||||
fn new(urls: &[Url]) -> crate::Result<Self> {
|
||||
fn new(workspaces: Vec<(Url, ClientSettings)>) -> crate::Result<Self> {
|
||||
Ok(Self(
|
||||
urls.iter()
|
||||
.map(Workspace::new)
|
||||
workspaces
|
||||
.into_iter()
|
||||
.map(|(url, settings)| Workspace::new(&url, settings))
|
||||
.collect::<crate::Result<_>>()?,
|
||||
))
|
||||
}
|
||||
|
||||
fn open_workspace_folder(&mut self, folder_url: &Url) -> crate::Result<()> {
|
||||
let (path, workspace) = Workspace::new(folder_url)?;
|
||||
// TODO(jane): find a way to allow for workspace settings to be updated dynamically
|
||||
let (path, workspace) = Workspace::new(folder_url, ClientSettings::default())?;
|
||||
self.0.insert(path, workspace);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -281,6 +292,24 @@ impl Workspaces {
|
|||
.close(url)
|
||||
}
|
||||
|
||||
fn client_settings(
|
||||
&self,
|
||||
url: &Url,
|
||||
global_settings: &ClientSettings,
|
||||
) -> ResolvedClientSettings {
|
||||
self.workspace_for_url(url).map_or_else(
|
||||
|| {
|
||||
tracing::warn!(
|
||||
"Workspace not found for {url}. Global settings will be used for this document"
|
||||
);
|
||||
ResolvedClientSettings::global(global_settings)
|
||||
},
|
||||
|workspace| {
|
||||
ResolvedClientSettings::with_workspace(&workspace.settings, global_settings)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn workspace_for_url(&self, url: &Url) -> Option<&Workspace> {
|
||||
Some(self.entry_for_url(url)?.1)
|
||||
}
|
||||
|
@ -307,7 +336,7 @@ impl Workspaces {
|
|||
}
|
||||
|
||||
impl Workspace {
|
||||
pub(crate) fn new(root: &Url) -> crate::Result<(PathBuf, Self)> {
|
||||
pub(crate) fn new(root: &Url, settings: ClientSettings) -> crate::Result<(PathBuf, Self)> {
|
||||
let path = root
|
||||
.to_file_path()
|
||||
.map_err(|()| anyhow!("workspace URL was not a file path!"))?;
|
||||
|
@ -319,6 +348,7 @@ impl Workspace {
|
|||
Self {
|
||||
open_documents: OpenDocuments::default(),
|
||||
configuration: Arc::new(configuration),
|
||||
settings,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
|
25
crates/ruff_server/src/session/capabilities.rs
Normal file
25
crates/ruff_server/src/session/capabilities.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use lsp_types::ClientCapabilities;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct ResolvedClientCapabilities {
|
||||
pub(crate) code_action_deferred_edit_resolution: bool,
|
||||
}
|
||||
|
||||
impl ResolvedClientCapabilities {
|
||||
pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
|
||||
let code_action_settings = client_capabilities
|
||||
.text_document
|
||||
.as_ref()
|
||||
.and_then(|doc_settings| doc_settings.code_action.as_ref());
|
||||
let code_action_data_support = code_action_settings
|
||||
.and_then(|code_action_settings| code_action_settings.data_support)
|
||||
.unwrap_or_default();
|
||||
let code_action_edit_resolution = code_action_settings
|
||||
.and_then(|code_action_settings| code_action_settings.resolve_support.as_ref())
|
||||
.is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into()));
|
||||
Self {
|
||||
code_action_deferred_edit_resolution: code_action_data_support
|
||||
&& code_action_edit_resolution,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +1,462 @@
|
|||
use lsp_types::ClientCapabilities;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct ResolvedClientCapabilities {
|
||||
pub(crate) code_action_deferred_edit_resolution: bool,
|
||||
use lsp_types::Url;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Maps a workspace URI to its associated client settings. Used during server initialization.
|
||||
pub(crate) type WorkspaceSettingsMap = FxHashMap<Url, ClientSettings>;
|
||||
|
||||
/// Resolved client settings for a specific document. These settings are meant to be
|
||||
/// used directly by the server, and are *not* a 1:1 representation with how the client
|
||||
/// sends them.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
// TODO(jane): Remove dead code warning
|
||||
#[allow(dead_code, clippy::struct_excessive_bools)]
|
||||
pub(crate) struct ResolvedClientSettings {
|
||||
fix_all: bool,
|
||||
organize_imports: bool,
|
||||
lint_enable: bool,
|
||||
disable_rule_comment_enable: bool,
|
||||
fix_violation_enable: bool,
|
||||
}
|
||||
|
||||
impl ResolvedClientCapabilities {
|
||||
pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
|
||||
let code_action_settings = client_capabilities
|
||||
.text_document
|
||||
.as_ref()
|
||||
.and_then(|doc_settings| doc_settings.code_action.as_ref());
|
||||
let code_action_data_support = code_action_settings
|
||||
.and_then(|code_action_settings| code_action_settings.data_support)
|
||||
.unwrap_or_default();
|
||||
let code_action_edit_resolution = code_action_settings
|
||||
.and_then(|code_action_settings| code_action_settings.resolve_support.as_ref())
|
||||
.is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into()));
|
||||
/// This is a direct representation of the settings schema sent by the client.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ClientSettings {
|
||||
fix_all: Option<bool>,
|
||||
organize_imports: Option<bool>,
|
||||
lint: Option<Lint>,
|
||||
code_action: Option<CodeAction>,
|
||||
}
|
||||
|
||||
/// This is a direct representation of the workspace settings schema,
|
||||
/// which inherits the schema of [`ClientSettings`] and adds extra fields
|
||||
/// to describe the workspace it applies to.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WorkspaceSettings {
|
||||
#[serde(flatten)]
|
||||
settings: ClientSettings,
|
||||
workspace: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Lint {
|
||||
enable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CodeAction {
|
||||
disable_rule_comment: Option<CodeActionSettings>,
|
||||
fix_violation: Option<CodeActionSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CodeActionSettings {
|
||||
enable: Option<bool>,
|
||||
}
|
||||
|
||||
/// This is the exact schema for initialization options sent in by the client
|
||||
/// during initialization.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(untagged)]
|
||||
enum InitializationOptions {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
HasWorkspaces {
|
||||
global_settings: ClientSettings,
|
||||
#[serde(rename = "settings")]
|
||||
workspace_settings: Vec<WorkspaceSettings>,
|
||||
},
|
||||
GlobalOnly {
|
||||
settings: Option<ClientSettings>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Built from the initialization options provided by the client.
|
||||
pub(crate) struct AllSettings {
|
||||
pub(crate) global_settings: ClientSettings,
|
||||
/// If this is `None`, the client only passed in global settings.
|
||||
pub(crate) workspace_settings: Option<WorkspaceSettingsMap>,
|
||||
}
|
||||
|
||||
impl AllSettings {
|
||||
/// Initializes the controller from the serialized initialization options.
|
||||
/// This fails if `options` are not valid initialization options.
|
||||
pub(crate) fn from_value(options: serde_json::Value) -> Self {
|
||||
Self::from_init_options(
|
||||
serde_json::from_value(options)
|
||||
.map_err(|err| {
|
||||
tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings...");
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn from_init_options(options: InitializationOptions) -> Self {
|
||||
let (global_settings, workspace_settings) = match options {
|
||||
InitializationOptions::GlobalOnly { settings } => (settings.unwrap_or_default(), None),
|
||||
InitializationOptions::HasWorkspaces {
|
||||
global_settings,
|
||||
workspace_settings,
|
||||
} => (global_settings, Some(workspace_settings)),
|
||||
};
|
||||
|
||||
Self {
|
||||
code_action_deferred_edit_resolution: code_action_data_support
|
||||
&& code_action_edit_resolution,
|
||||
global_settings,
|
||||
workspace_settings: workspace_settings.map(|workspace_settings| {
|
||||
workspace_settings
|
||||
.into_iter()
|
||||
.map(|settings| (settings.workspace, settings.settings))
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedClientSettings {
|
||||
/// Resolves a series of client settings, prioritizing workspace settings over global settings.
|
||||
/// Any fields not specified by either are set to their defaults.
|
||||
pub(super) fn with_workspace(
|
||||
workspace_settings: &ClientSettings,
|
||||
global_settings: &ClientSettings,
|
||||
) -> Self {
|
||||
Self::new_impl(&[workspace_settings, global_settings])
|
||||
}
|
||||
|
||||
/// Resolves global settings only.
|
||||
pub(super) fn global(global_settings: &ClientSettings) -> Self {
|
||||
Self::new_impl(&[global_settings])
|
||||
}
|
||||
|
||||
fn new_impl(all_settings: &[&ClientSettings]) -> Self {
|
||||
Self {
|
||||
fix_all: Self::resolve_or(all_settings, |settings| settings.fix_all, true),
|
||||
organize_imports: Self::resolve_or(
|
||||
all_settings,
|
||||
|settings| settings.organize_imports,
|
||||
true,
|
||||
),
|
||||
lint_enable: Self::resolve_or(
|
||||
all_settings,
|
||||
|settings| settings.lint.as_ref()?.enable,
|
||||
true,
|
||||
),
|
||||
disable_rule_comment_enable: Self::resolve_or(
|
||||
all_settings,
|
||||
|settings| {
|
||||
settings
|
||||
.code_action
|
||||
.as_ref()?
|
||||
.disable_rule_comment
|
||||
.as_ref()?
|
||||
.enable
|
||||
},
|
||||
true,
|
||||
),
|
||||
fix_violation_enable: Self::resolve_or(
|
||||
all_settings,
|
||||
|settings| {
|
||||
settings
|
||||
.code_action
|
||||
.as_ref()?
|
||||
.fix_violation
|
||||
.as_ref()?
|
||||
.enable
|
||||
},
|
||||
true,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve a setting using a list of available client settings as sources.
|
||||
/// Client settings that come earlier in the list take priority. `default` will be returned
|
||||
/// if none of the settings specify the requested setting.
|
||||
fn resolve_or<T>(
|
||||
all_settings: &[&ClientSettings],
|
||||
get: impl Fn(&ClientSettings) -> Option<T>,
|
||||
default: T,
|
||||
) -> T {
|
||||
all_settings
|
||||
.iter()
|
||||
.map(Deref::deref)
|
||||
.find_map(get)
|
||||
.unwrap_or(default)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InitializationOptions {
|
||||
fn default() -> Self {
|
||||
Self::GlobalOnly { settings: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use super::*;
|
||||
|
||||
const VS_CODE_INIT_OPTIONS_FIXTURE: &str =
|
||||
include_str!("../../resources/test/fixtures/settings/vs_code_initialization_options.json");
|
||||
const GLOBAL_ONLY_INIT_OPTIONS_FIXTURE: &str =
|
||||
include_str!("../../resources/test/fixtures/settings/global_only.json");
|
||||
const EMPTY_INIT_OPTIONS_FIXTURE: &str =
|
||||
include_str!("../../resources/test/fixtures/settings/empty.json");
|
||||
|
||||
fn deserialize_fixture<T: DeserializeOwned>(content: &str) -> T {
|
||||
serde_json::from_str(content).expect("test fixture JSON should deserialize")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vs_code_init_options_deserialize() {
|
||||
let options: InitializationOptions = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE);
|
||||
|
||||
assert_debug_snapshot!(options, @r###"
|
||||
HasWorkspaces {
|
||||
global_settings: ClientSettings {
|
||||
fix_all: Some(
|
||||
false,
|
||||
),
|
||||
organize_imports: Some(
|
||||
true,
|
||||
),
|
||||
lint: Some(
|
||||
Lint {
|
||||
enable: Some(
|
||||
true,
|
||||
),
|
||||
},
|
||||
),
|
||||
code_action: Some(
|
||||
CodeAction {
|
||||
disable_rule_comment: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
false,
|
||||
),
|
||||
},
|
||||
),
|
||||
fix_violation: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
false,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
workspace_settings: [
|
||||
WorkspaceSettings {
|
||||
settings: ClientSettings {
|
||||
fix_all: Some(
|
||||
true,
|
||||
),
|
||||
organize_imports: Some(
|
||||
true,
|
||||
),
|
||||
lint: Some(
|
||||
Lint {
|
||||
enable: Some(
|
||||
true,
|
||||
),
|
||||
},
|
||||
),
|
||||
code_action: Some(
|
||||
CodeAction {
|
||||
disable_rule_comment: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
false,
|
||||
),
|
||||
},
|
||||
),
|
||||
fix_violation: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
false,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
workspace: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "/Users/test/projects/pandas",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
},
|
||||
WorkspaceSettings {
|
||||
settings: ClientSettings {
|
||||
fix_all: Some(
|
||||
true,
|
||||
),
|
||||
organize_imports: Some(
|
||||
true,
|
||||
),
|
||||
lint: Some(
|
||||
Lint {
|
||||
enable: Some(
|
||||
true,
|
||||
),
|
||||
},
|
||||
),
|
||||
code_action: Some(
|
||||
CodeAction {
|
||||
disable_rule_comment: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
true,
|
||||
),
|
||||
},
|
||||
),
|
||||
fix_violation: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
false,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
workspace: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "/Users/test/projects/scipy",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vs_code_workspace_settings_resolve() {
|
||||
let options = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE);
|
||||
let AllSettings {
|
||||
global_settings,
|
||||
workspace_settings,
|
||||
} = AllSettings::from_init_options(options);
|
||||
let url = Url::parse("file:///Users/test/projects/pandas").expect("url should parse");
|
||||
let workspace_settings = workspace_settings.expect("workspace settings should exist");
|
||||
assert_eq!(
|
||||
ResolvedClientSettings::with_workspace(
|
||||
workspace_settings
|
||||
.get(&url)
|
||||
.expect("workspace setting should exist"),
|
||||
&global_settings
|
||||
),
|
||||
ResolvedClientSettings {
|
||||
fix_all: true,
|
||||
organize_imports: true,
|
||||
lint_enable: true,
|
||||
disable_rule_comment_enable: false,
|
||||
fix_violation_enable: false,
|
||||
}
|
||||
);
|
||||
let url = Url::parse("file:///Users/test/projects/scipy").expect("url should parse");
|
||||
assert_eq!(
|
||||
ResolvedClientSettings::with_workspace(
|
||||
workspace_settings
|
||||
.get(&url)
|
||||
.expect("workspace setting should exist"),
|
||||
&global_settings
|
||||
),
|
||||
ResolvedClientSettings {
|
||||
fix_all: true,
|
||||
organize_imports: true,
|
||||
lint_enable: true,
|
||||
disable_rule_comment_enable: true,
|
||||
fix_violation_enable: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_only_init_options_deserialize() {
|
||||
let options: InitializationOptions = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE);
|
||||
|
||||
assert_debug_snapshot!(options, @r###"
|
||||
GlobalOnly {
|
||||
settings: Some(
|
||||
ClientSettings {
|
||||
fix_all: Some(
|
||||
false,
|
||||
),
|
||||
organize_imports: None,
|
||||
lint: Some(
|
||||
Lint {
|
||||
enable: None,
|
||||
},
|
||||
),
|
||||
code_action: Some(
|
||||
CodeAction {
|
||||
disable_rule_comment: Some(
|
||||
CodeActionSettings {
|
||||
enable: Some(
|
||||
false,
|
||||
),
|
||||
},
|
||||
),
|
||||
fix_violation: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_only_resolves_correctly() {
|
||||
let options = deserialize_fixture(GLOBAL_ONLY_INIT_OPTIONS_FIXTURE);
|
||||
|
||||
let AllSettings {
|
||||
global_settings, ..
|
||||
} = AllSettings::from_init_options(options);
|
||||
assert_eq!(
|
||||
ResolvedClientSettings::global(&global_settings),
|
||||
ResolvedClientSettings {
|
||||
fix_all: false,
|
||||
organize_imports: true,
|
||||
lint_enable: true,
|
||||
disable_rule_comment_enable: false,
|
||||
fix_violation_enable: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_init_options_deserialize() {
|
||||
let options: InitializationOptions = deserialize_fixture(EMPTY_INIT_OPTIONS_FIXTURE);
|
||||
|
||||
assert_eq!(options, InitializationOptions::default());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue