ruff server: Fix multiple issues with Neovim and Helix (#11497)

## Summary

Fixes https://github.com/astral-sh/ruff/issues/11236.

This PR fixes several issues, most of which relate to non-VS Code
editors (Helix and Neovim).

1. Global-only initialization options are now correctly deserialized
from Neovim and Helix
2. Empty diagnostics are now published correctly for Neovim and Helix.
3. A workspace folder is created at the current working directory if the
initialization parameters send an empty list of workspace folders.
4. The server now gracefully handles opening files outside of any known
workspace, and will use global fallback settings taken from client
editor settings and a user settings TOML, if it exists.

## Test Plan

I've tested to confirm that each issue has been fixed.

* Global-only initialization options are now correctly deserialized from
Neovim and Helix + the server gracefully handles opening files outside
of any known workspace


4f33477f-20c8-4e50-8214-6608b1a1ea6b

* Empty diagnostics are now published correctly for Neovim and Helix


c93f56a0-f75d-466f-9f40-d77f99cf0637

* A workspace folder is created at the current working directory if the
initialization parameters send an empty list of workspace folders.



b4b2e818-4b0d-40ce-961d-5831478cc726
This commit is contained in:
Jane Lewis 2024-05-22 13:50:58 -07:00 committed by GitHub
parent 519a65007f
commit 94abea4b08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 120 additions and 101 deletions

View file

@ -1,5 +1,4 @@
{ {
"settings": {
"codeAction": { "codeAction": {
"disableRuleComment": { "disableRuleComment": {
"enable": false "enable": false
@ -14,4 +13,3 @@
"lineLength": 80, "lineLength": 80,
"exclude": ["third_party"] "exclude": ["third_party"]
} }
}

View file

@ -119,12 +119,14 @@ pub(crate) fn check(
let mut diagnostics = Diagnostics::default(); let mut diagnostics = Diagnostics::default();
// Populate all cell URLs with an empty diagnostic list. // Populates all relevant URLs with an empty diagnostic list.
// This ensures that cells without diagnostics still get updated. // This ensures that documents without diagnostics still get updated.
if let Some(notebook) = query.as_notebook() { if let Some(notebook) = query.as_notebook() {
for url in notebook.urls() { for url in notebook.urls() {
diagnostics.entry(url.clone()).or_default(); diagnostics.entry(url.clone()).or_default();
} }
} else {
diagnostics.entry(query.make_key().into_url()).or_default();
} }
let lsp_diagnostics = data let lsp_diagnostics = data

View file

@ -69,7 +69,11 @@ impl Server {
let AllSettings { let AllSettings {
global_settings, global_settings,
mut workspace_settings, mut workspace_settings,
} = AllSettings::from_value(init_params.initialization_options.unwrap_or_default()); } = AllSettings::from_value(
init_params
.initialization_options
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);
let mut workspace_for_path = |path: PathBuf| { let mut workspace_for_path = |path: PathBuf| {
let Some(workspace_settings) = workspace_settings.as_mut() else { let Some(workspace_settings) = workspace_settings.as_mut() else {
@ -84,11 +88,12 @@ impl Server {
let workspaces = init_params let workspaces = init_params
.workspace_folders .workspace_folders
.filter(|folders| !folders.is_empty())
.map(|folders| folders.into_iter().map(|folder| { .map(|folders| folders.into_iter().map(|folder| {
workspace_for_path(folder.uri.to_file_path().unwrap()) workspace_for_path(folder.uri.to_file_path().unwrap())
}).collect()) }).collect())
.or_else(|| { .or_else(|| {
tracing::debug!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace..."); tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace...");
let uri = 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_path(uri.to_file_path().unwrap())]) Some(vec![workspace_for_path(uri.to_file_path().unwrap())])
}) })

View file

@ -67,7 +67,7 @@ impl Session {
Some(DocumentSnapshot { Some(DocumentSnapshot {
resolved_client_capabilities: self.resolved_client_capabilities.clone(), resolved_client_capabilities: self.resolved_client_capabilities.clone(),
client_settings: self.index.client_settings(&key, &self.global_settings), client_settings: self.index.client_settings(&key, &self.global_settings),
document_ref: self.index.make_document_ref(key)?, document_ref: self.index.make_document_ref(key, &self.global_settings)?,
position_encoding: self.position_encoding, position_encoding: self.position_encoding,
}) })
} }

View file

@ -237,12 +237,27 @@ impl Index {
Ok(()) Ok(())
} }
pub(super) fn make_document_ref(&self, key: DocumentKey) -> Option<DocumentQuery> { pub(super) fn make_document_ref(
&self,
key: DocumentKey,
global_settings: &ClientSettings,
) -> Option<DocumentQuery> {
let path = self.path_for_key(&key)?.clone(); let path = self.path_for_key(&key)?.clone();
let document_settings = self let document_settings = self
.settings_for_path(&path)? .settings_for_path(&path)
.workspace_settings_index .map(|settings| settings.workspace_settings_index.get(&path))
.get(&path); .unwrap_or_else(|| {
tracing::warn!(
"No settings available for {} - falling back to default settings",
path.display()
);
let resolved_global = ResolvedClientSettings::global(global_settings);
let root = path.parent().unwrap_or(&path);
Arc::new(RuffSettings::fallback(
resolved_global.editor_settings(),
root,
))
});
let controller = self.documents.get(&path)?; let controller = self.documents.get(&path)?;
let cell_uri = match key { let cell_uri = match key {

View file

@ -43,6 +43,32 @@ impl std::fmt::Display for RuffSettings {
} }
impl RuffSettings { impl RuffSettings {
pub(crate) fn fallback(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings {
let fallback = find_user_settings_toml()
.and_then(|user_settings| {
ruff_workspace::resolver::resolve_root_settings(
&user_settings,
Relativity::Cwd,
&EditorConfigurationTransformer(editor_settings, root),
)
.ok()
})
.unwrap_or_else(|| {
let default_configuration = ruff_workspace::configuration::Configuration::default();
EditorConfigurationTransformer(editor_settings, root)
.transform(default_configuration)
.into_settings(root)
.expect(
"editor configuration should merge successfully with default configuration",
)
});
RuffSettings {
formatter: fallback.formatter,
linter: fallback.linter,
}
}
pub(crate) fn linter(&self) -> &ruff_linter::settings::LinterSettings { pub(crate) fn linter(&self) -> &ruff_linter::settings::LinterSettings {
&self.linter &self.linter
} }
@ -80,32 +106,9 @@ impl RuffSettingsIndex {
} }
} }
let fallback = find_user_settings_toml() let fallback = Arc::new(RuffSettings::fallback(editor_settings, root));
.and_then(|user_settings| {
ruff_workspace::resolver::resolve_root_settings(
&user_settings,
Relativity::Cwd,
&EditorConfigurationTransformer(editor_settings, root),
)
.ok()
})
.unwrap_or_else(|| {
let default_configuration = ruff_workspace::configuration::Configuration::default();
EditorConfigurationTransformer(editor_settings, root)
.transform(default_configuration)
.into_settings(root)
.expect(
"editor configuration should merge successfully with default configuration",
)
});
Self { Self { index, fallback }
index,
fallback: Arc::new(RuffSettings {
formatter: fallback.formatter,
linter: fallback.linter,
}),
}
} }
pub(super) fn get(&self, document_path: &Path) -> Arc<RuffSettings> { pub(super) fn get(&self, document_path: &Path) -> Arc<RuffSettings> {
@ -118,11 +121,6 @@ impl RuffSettingsIndex {
return settings.clone(); return settings.clone();
} }
tracing::info!(
"No Ruff settings file found for {}; falling back to default configuration",
document_path.display()
);
self.fallback.clone() self.fallback.clone()
} }
} }

View file

@ -130,7 +130,8 @@ enum InitializationOptions {
workspace_settings: Vec<WorkspaceSettings>, workspace_settings: Vec<WorkspaceSettings>,
}, },
GlobalOnly { GlobalOnly {
settings: Option<ClientSettings>, #[serde(flatten)]
settings: ClientSettings,
}, },
} }
@ -157,7 +158,7 @@ impl AllSettings {
fn from_init_options(options: InitializationOptions) -> Self { fn from_init_options(options: InitializationOptions) -> Self {
let (global_settings, workspace_settings) = match options { let (global_settings, workspace_settings) = match options {
InitializationOptions::GlobalOnly { settings } => (settings.unwrap_or_default(), None), InitializationOptions::GlobalOnly { settings } => (settings, None),
InitializationOptions::HasWorkspaces { InitializationOptions::HasWorkspaces {
global_settings, global_settings,
workspace_settings, workspace_settings,
@ -341,7 +342,9 @@ impl ResolvedClientSettings {
impl Default for InitializationOptions { impl Default for InitializationOptions {
fn default() -> Self { fn default() -> Self {
Self::GlobalOnly { settings: None } Self::GlobalOnly {
settings: ClientSettings::default(),
}
} }
} }
@ -626,8 +629,7 @@ mod tests {
assert_debug_snapshot!(options, @r###" assert_debug_snapshot!(options, @r###"
GlobalOnly { GlobalOnly {
settings: Some( settings: ClientSettings {
ClientSettings {
configuration: None, configuration: None,
fix_all: Some( fix_all: Some(
false, false,
@ -671,7 +673,6 @@ mod tests {
), ),
configuration_preference: None, configuration_preference: None,
}, },
),
} }
"###); "###);
} }