mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:43 +00:00
Avoid indexing the workspace for single-file mode (#13770)
## Summary This PR updates the language server to avoid indexing the workspace for single-file mode. **What's a single-file mode?** When a user opens the file directly in an editor, and not the folder that represents the workspace, the editor usually can't determine the workspace root. This means that during initializing the server, the `workspaceFolders` field will be empty / nil. Now, in this case, the server defaults to using the current working directory which is a reasonable default assuming that the directory would point to the one where this open file is present. This would allow the server to index the directory itself for any config file, if present. It turns out that in VS Code the current working directory in the above scenario is the system root directory `/` and so the server will try to index the entire root directory which would take a lot of time. This is the issue as described in https://github.com/astral-sh/ruff-vscode/issues/627. To reproduce, refer https://github.com/astral-sh/ruff-vscode/issues/627#issuecomment-2401440767. This PR updates the indexer to avoid traversing the workspace to read any config file that might be present. The first commit (8dd2a31eef
) refactors the initialization and introduces two structs `Workspaces` and `Workspace`. The latter struct includes a field to determine whether it's the default workspace. The second commit (61fc39bdb6
) utilizes this field to avoid traversing. Closes: #11366 ## Editor behavior This is to document the behavior as seen in different editors. The test scenario used has the following directory tree structure: ``` . ├── nested │ ├── nested.py │ └── pyproject.toml └── test.py ``` where, the contents of the files are: **test.py** ```py import os ``` **nested/nested.py** ```py import os import math ``` **nested/pyproject.toml** ```toml [tool.ruff.lint] select = ["I"] ``` Steps: 1. Open `test.py` directly in the editor 2. Validate that it raises the `F401` violation 3. Open `nested/nested.py` in the same editor instance 4. This file would raise only `I001` if the `nested/pyproject.toml` was indexed ### VS Code When (1) is done from above, the current working directory is `/` which means the server will try to index the entire system to build up the settings index. This will include the `nested/pyproject.toml` file as well. This leads to bad user experience because the user would need to wait for minutes for the server to finish indexing. This PR avoids that by not traversing the workspace directory in single-file mode. But, in VS Code, this means that per (4), the file wouldn't raise `I001` but only raise two `F401` violations because the `nested/pyproject.toml` was never resolved. One solution here would be to fix this in the extension itself where we would detect this scenario and pass in the workspace directory that is the one containing this open file in (1) above. ### Neovim **tl;dr** it works as expected because the client considers the presence of certain files (depending on the server) as the root of the workspace. For Ruff, they are `pyproject.toml`, `ruff.toml`, and `.ruff.toml`. This means that the client notifies us as the user moves between single-file mode and workspace mode. https://github.com/astral-sh/ruff/pull/13770#issuecomment-2416608055 ### Helix Same as Neovim, additional context in https://github.com/astral-sh/ruff/pull/13770#issuecomment-2417362097 ### Sublime Text **tl;dr** It works similar to VS Code except that the current working directory of the current process is different and thus the config file is never read. So, the behavior remains unchanged with this PR. https://github.com/astral-sh/ruff/pull/13770#issuecomment-2417362097 ### Zed Zed seems to be starting a separate language server instance for each file when the editor is running in a single-file mode even though all files have been opened in a single editor instance. (Separated the logs into sections separated by a single blank line indicating 3 different server instances that the editor started for 3 files.) ``` 0.000053375s INFO main ruff_server::server: No workspace settings found for file:///Users/dhruv/projects/ruff-temp, using default settings 0.009448792s INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/projects/ruff-temp 0.009906334s DEBUG ruff:main ruff_server::resolve: Included path via `include`: /Users/dhruv/projects/ruff-temp/test.py 0.011775917s INFO ruff:main ruff_server::server: Configuration file watcher successfully registered 0.000060583s INFO main ruff_server::server: No workspace settings found for file:///Users/dhruv/projects/ruff-temp/nested, using default settings 0.010387125s INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/projects/ruff-temp/nested 0.011061875s DEBUG ruff:main ruff_server::resolve: Included path via `include`: /Users/dhruv/projects/ruff-temp/nested/nested.py 0.011545208s INFO ruff:main ruff_server::server: Configuration file watcher successfully registered 0.000059125s INFO main ruff_server::server: No workspace settings found for file:///Users/dhruv/projects/ruff-temp/nested, using default settings 0.010857583s INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/projects/ruff-temp/nested 0.011428958s DEBUG ruff:main ruff_server::resolve: Included path via `include`: /Users/dhruv/projects/ruff-temp/nested/other.py 0.011893792s INFO ruff:main ruff_server::server: Configuration file watcher successfully registered ``` ## Test Plan When using the `ruff` server from this PR, we see that the server starts quickly as seen in the logs. Next, when I switch to the release binary, it starts indexing the root directory. For more details, refer to the "Editor Behavior" section above.
This commit is contained in:
parent
3d0bdb426a
commit
040a591cad
9 changed files with 211 additions and 53 deletions
|
@ -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<Workspace>);
|
||||
|
||||
impl Workspaces {
|
||||
pub fn new(workspaces: Vec<Workspace>) -> 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<Vec<WorkspaceFolder>>,
|
||||
mut workspace_settings: WorkspaceSettingsMap,
|
||||
) -> std::result::Result<Workspaces, WorkspacesError> {
|
||||
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<ClientSettings>,
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue