diff --git a/Cargo.lock b/Cargo.lock index 9fd3cb9b621df..0241301062db4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2833,6 +2833,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "thiserror", "tracing", "tracing-subscriber", ] diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 6f48754969ef7..c9678e1a12b32 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -37,6 +37,7 @@ rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } shellexpand = { workspace = true } +thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 58b501742a2b8..ff0dc7ddb59b2 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -2,7 +2,7 @@ pub use edit::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument}; use lsp_types::CodeActionKind; -pub use server::Server; +pub use server::{Server, Workspace, Workspaces}; pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session}; #[macro_use] diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 015ba9de3eddc..2674e76a04b04 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -2,9 +2,13 @@ use lsp_server as lsp; use lsp_types as types; +use lsp_types::InitializeParams; +use lsp_types::WorkspaceFolder; use std::num::NonZeroUsize; +use std::ops::Deref; use std::panic::PanicInfo; use std::str::FromStr; +use thiserror::Error; use types::ClientCapabilities; use types::CodeActionKind; use types::CodeActionOptions; @@ -18,6 +22,7 @@ use types::OneOf; use types::TextDocumentSyncCapability; use types::TextDocumentSyncKind; use types::TextDocumentSyncOptions; +use types::Url; use types::WorkDoneProgressOptions; use types::WorkspaceFoldersServerCapabilities; @@ -29,6 +34,7 @@ use self::schedule::Task; use crate::session::AllSettings; use crate::session::ClientSettings; use crate::session::Session; +use crate::session::WorkspaceSettingsMap; use crate::PositionEncoding; mod api; @@ -71,9 +77,15 @@ impl Server { crate::message::init_messenger(connection.make_sender()); + let InitializeParams { + initialization_options, + workspace_folders, + client_info, + .. + } = init_params; + let mut all_settings = AllSettings::from_value( - init_params - .initialization_options + initialization_options .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())), ); if let Some(preview) = preview { @@ -81,7 +93,7 @@ impl Server { } let AllSettings { global_settings, - mut workspace_settings, + workspace_settings, } = all_settings; crate::trace::init_tracing( @@ -91,34 +103,13 @@ impl Server { .log_level .unwrap_or(crate::trace::LogLevel::Info), global_settings.tracing.log_file.as_deref(), - init_params.client_info.as_ref(), + client_info.as_ref(), ); - let mut workspace_for_url = |url: lsp_types::Url| { - let Some(workspace_settings) = workspace_settings.as_mut() else { - return (url, ClientSettings::default()); - }; - let settings = workspace_settings.remove(&url).unwrap_or_else(|| { - tracing::warn!("No workspace settings found for {}", url); - ClientSettings::default() - }); - (url, settings) - }; - - let workspaces = init_params - .workspace_folders - .filter(|folders| !folders.is_empty()) - .map(|folders| folders.into_iter().map(|folder| { - workspace_for_url(folder.uri) - }).collect()) - .or_else(|| { - 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()?; - Some(vec![workspace_for_url(uri)]) - }) - .ok_or_else(|| { - anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.") - })?; + let workspaces = Workspaces::from_workspace_folders( + workspace_folders, + workspace_settings.unwrap_or_default(), + )?; Ok(Self { connection, @@ -127,7 +118,7 @@ impl Server { &client_capabilities, position_encoding, global_settings, - workspaces, + &workspaces, )?, client_capabilities, }) @@ -462,3 +453,122 @@ impl FromStr for SupportedCommand { }) } } + +#[derive(Debug)] +pub struct Workspaces(Vec); + +impl Workspaces { + pub fn new(workspaces: Vec) -> Self { + Self(workspaces) + } + + /// Create the workspaces from the provided workspace folders as provided by the client during + /// initialization. + fn from_workspace_folders( + workspace_folders: Option>, + mut workspace_settings: WorkspaceSettingsMap, + ) -> std::result::Result { + let mut client_settings_for_url = |url: &Url| { + workspace_settings.remove(url).unwrap_or_else(|| { + tracing::info!( + "No workspace settings found for {}, using default settings", + url + ); + ClientSettings::default() + }) + }; + + let workspaces = + if let Some(folders) = workspace_folders.filter(|folders| !folders.is_empty()) { + folders + .into_iter() + .map(|folder| { + let settings = client_settings_for_url(&folder.uri); + Workspace::new(folder.uri).with_settings(settings) + }) + .collect() + } else { + let current_dir = std::env::current_dir().map_err(WorkspacesError::Io)?; + tracing::info!( + "No workspace(s) were provided during initialization. \ + Using the current working directory as a default workspace: {}", + current_dir.display() + ); + let uri = Url::from_file_path(current_dir) + .map_err(|()| WorkspacesError::InvalidCurrentDir)?; + let settings = client_settings_for_url(&uri); + vec![Workspace::default(uri).with_settings(settings)] + }; + + Ok(Workspaces(workspaces)) + } +} + +impl Deref for Workspaces { + type Target = [Workspace]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Error, Debug)] +enum WorkspacesError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Failed to create a URL from the current working directory")] + InvalidCurrentDir, +} + +#[derive(Debug)] +pub struct Workspace { + /// The [`Url`] pointing to the root of the workspace. + url: Url, + /// The client settings for this workspace. + settings: Option, + /// Whether this is the default workspace as created by the server. This will be the case when + /// no workspace folders were provided during initialization. + is_default: bool, +} + +impl Workspace { + /// Create a new workspace with the given root URL. + pub fn new(url: Url) -> Self { + Self { + url, + settings: None, + is_default: false, + } + } + + /// Create a new default workspace with the given root URL. + pub fn default(url: Url) -> Self { + Self { + url, + settings: None, + is_default: true, + } + } + + /// Set the client settings for this workspace. + #[must_use] + pub fn with_settings(mut self, settings: ClientSettings) -> Self { + self.settings = Some(settings); + self + } + + /// Returns the root URL of the workspace. + pub(crate) fn url(&self) -> &Url { + &self.url + } + + /// Returns the client settings for this workspace. + pub(crate) fn settings(&self) -> Option<&ClientSettings> { + self.settings.as_ref() + } + + /// Returns true if this is the default workspace. + pub(crate) fn is_default(&self) -> bool { + self.is_default + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs index 6b9235799332c..a10b376a64de6 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs @@ -20,7 +20,7 @@ impl super::SyncNotificationHandler for DidChangeWorkspace { ) -> Result<()> { for types::WorkspaceFolder { uri, .. } in params.event.added { session - .open_workspace_folder(&uri) + .open_workspace_folder(uri) .with_failure_code(lsp_server::ErrorCode::InvalidParams)?; } for types::WorkspaceFolder { uri, .. } in params.event.removed { diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index fb01d4fac53eb..23a60f26ff2e6 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -5,12 +5,13 @@ use std::sync::Arc; use lsp_types::{ClientCapabilities, NotebookDocumentCellChange, Url}; use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument}; +use crate::server::Workspaces; use crate::{PositionEncoding, TextDocument}; pub(crate) use self::capabilities::ResolvedClientCapabilities; pub use self::index::DocumentQuery; -pub(crate) use self::settings::AllSettings; pub use self::settings::ClientSettings; +pub(crate) use self::settings::{AllSettings, WorkspaceSettingsMap}; mod capabilities; mod index; @@ -42,11 +43,11 @@ impl Session { client_capabilities: &ClientCapabilities, position_encoding: PositionEncoding, global_settings: ClientSettings, - workspace_folders: Vec<(Url, ClientSettings)>, + workspaces: &Workspaces, ) -> crate::Result { Ok(Self { position_encoding, - index: index::Index::new(workspace_folders, &global_settings)?, + index: index::Index::new(workspaces, &global_settings)?, global_settings, resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( client_capabilities, @@ -136,7 +137,7 @@ impl Session { } /// Open a workspace folder at the given `url`. - pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + pub(crate) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> { self.index.open_workspace_folder(url, &self.global_settings) } diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index 6835eabbb739e..a4adbd4a17ccb 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -10,6 +10,7 @@ use rustc_hash::FxHashMap; pub(crate) use ruff_settings::RuffSettings; use crate::edit::LanguageId; +use crate::server::{Workspace, Workspaces}; use crate::{ edit::{DocumentKey, DocumentVersion, NotebookDocument}, PositionEncoding, TextDocument, @@ -67,12 +68,12 @@ pub enum DocumentQuery { impl Index { pub(super) fn new( - workspace_folders: Vec<(Url, ClientSettings)>, + workspaces: &Workspaces, global_settings: &ClientSettings, ) -> crate::Result { let mut settings = WorkspaceSettingsIndex::default(); - for (url, workspace_settings) in workspace_folders { - settings.register_workspace(&url, Some(workspace_settings), global_settings)?; + for workspace in &**workspaces { + settings.register_workspace(workspace, global_settings)?; } Ok(Self { @@ -167,11 +168,12 @@ impl Index { pub(super) fn open_workspace_folder( &mut self, - url: &Url, + url: Url, global_settings: &ClientSettings, ) -> crate::Result<()> { // TODO(jane): Find a way for workspace client settings to be added or changed dynamically. - self.settings.register_workspace(url, None, global_settings) + self.settings + .register_workspace(&Workspace::new(url), global_settings) } pub(super) fn num_documents(&self) -> usize { @@ -284,6 +286,7 @@ impl Index { settings.ruff_settings = ruff_settings::RuffSettingsIndex::new( root, settings.client_settings.editor_settings(), + false, ); } } @@ -398,10 +401,10 @@ impl WorkspaceSettingsIndex { /// workspace. Otherwise, the global settings are used exclusively. fn register_workspace( &mut self, - workspace_url: &Url, - workspace_settings: Option, + workspace: &Workspace, global_settings: &ClientSettings, ) -> crate::Result<()> { + let workspace_url = workspace.url(); if workspace_url.scheme() != "file" { tracing::info!("Ignoring non-file workspace URL: {workspace_url}"); show_warn_msg!("Ruff does not support non-file workspaces; Ignoring {workspace_url}"); @@ -411,8 +414,8 @@ impl WorkspaceSettingsIndex { anyhow!("Failed to convert workspace URL to file path: {workspace_url}") })?; - let client_settings = if let Some(workspace_settings) = workspace_settings { - ResolvedClientSettings::with_workspace(&workspace_settings, global_settings) + let client_settings = if let Some(workspace_settings) = workspace.settings() { + ResolvedClientSettings::with_workspace(workspace_settings, global_settings) } else { ResolvedClientSettings::global(global_settings) }; @@ -420,8 +423,10 @@ impl WorkspaceSettingsIndex { let workspace_settings_index = ruff_settings::RuffSettingsIndex::new( &workspace_path, client_settings.editor_settings(), + workspace.is_default(), ); + tracing::info!("Registering workspace: {}", workspace_path.display()); self.insert( workspace_path, WorkspaceSettings { diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index a564c75d000db..8a783f4497fec 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -100,13 +100,33 @@ impl RuffSettings { } impl RuffSettingsIndex { - pub(super) fn new(root: &Path, editor_settings: &ResolvedEditorSettings) -> Self { + /// Create the settings index for the given workspace root. + /// + /// This will create the index in the following order: + /// 1. Resolve any settings from above the workspace root + /// 2. Resolve any settings from the workspace root itself + /// 3. Resolve any settings from within the workspace directory tree + /// + /// If this is the default workspace i.e., the client did not specify any workspace and so the + /// server will be running in a single file mode, then only (1) and (2) will be resolved, + /// skipping (3). + pub(super) fn new( + root: &Path, + editor_settings: &ResolvedEditorSettings, + is_default_workspace: bool, + ) -> Self { let mut has_error = false; let mut index = BTreeMap::default(); let mut respect_gitignore = None; - // Add any settings from above the workspace root, excluding the workspace root itself. - for directory in root.ancestors().skip(1) { + // If this is *not* the default workspace, then we should skip the workspace root itself + // because it will be resolved when walking the workspace directory tree. This is done by + // the `WalkBuilder` below. + let should_skip_workspace = usize::from(!is_default_workspace); + + // Add any settings from above the workspace root, skipping the workspace root itself if + // this is *not* the default workspace. + for directory in root.ancestors().skip(should_skip_workspace) { match settings_toml(directory) { Ok(Some(pyproject)) => { match ruff_workspace::resolver::resolve_root_settings( @@ -156,6 +176,26 @@ impl RuffSettingsIndex { let fallback = Arc::new(RuffSettings::fallback(editor_settings, root)); + // If this is the default workspace, the server is running in single-file mode. What this + // means is that the user opened a file directly (not the folder) in the editor and the + // server didn't receive a workspace folder during initialization. In this case, we default + // to the current working directory and skip walking the workspace directory tree for any + // settings. + // + // Refer to https://github.com/astral-sh/ruff/pull/13770 to understand what this behavior + // means for different editors. + if is_default_workspace { + if has_error { + let root = root.display(); + show_err_msg!( + "Error while resolving settings from workspace {root}. \ + Please refer to the logs for more details.", + ); + } + + return RuffSettingsIndex { index, fallback }; + } + // Add any settings within the workspace itself let mut builder = WalkBuilder::new(root); builder.standard_filters( @@ -266,7 +306,7 @@ impl RuffSettingsIndex { ); } - Self { + RuffSettingsIndex { index: index.into_inner().unwrap(), fallback, } diff --git a/crates/ruff_server/tests/notebook.rs b/crates/ruff_server/tests/notebook.rs index 6bca95c023d3e..52a63b19193e4 100644 --- a/crates/ruff_server/tests/notebook.rs +++ b/crates/ruff_server/tests/notebook.rs @@ -8,7 +8,7 @@ use lsp_types::{ Position, Range, TextDocumentContentChangeEvent, VersionedTextDocumentIdentifier, }; use ruff_notebook::SourceValue; -use ruff_server::ClientSettings; +use ruff_server::{ClientSettings, Workspace, Workspaces}; const SUPER_RESOLUTION_OVERVIEW_PATH: &str = "./resources/test/fixtures/tensorflow_test_notebook.ipynb"; @@ -32,10 +32,10 @@ fn super_resolution_overview() { &ClientCapabilities::default(), ruff_server::PositionEncoding::UTF16, ClientSettings::default(), - vec![( + &Workspaces::new(vec![Workspace::new( lsp_types::Url::from_file_path(file_path.parent().unwrap()).unwrap(), - ClientSettings::default(), - )], + ) + .with_settings(ClientSettings::default())]), ) .unwrap();