mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:43 +00:00

## 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.
187 lines
6.5 KiB
Rust
187 lines
6.5 KiB
Rust
//! Data model, state management, and configuration resolution.
|
|
|
|
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 use self::settings::ClientSettings;
|
|
pub(crate) use self::settings::{AllSettings, WorkspaceSettingsMap};
|
|
|
|
mod capabilities;
|
|
mod index;
|
|
mod settings;
|
|
|
|
/// The global state for the LSP
|
|
pub struct Session {
|
|
/// Used to retrieve information about open documents and settings.
|
|
index: index::Index,
|
|
/// 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>,
|
|
}
|
|
|
|
/// An immutable snapshot of `Session` that references
|
|
/// a specific document.
|
|
pub struct DocumentSnapshot {
|
|
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
|
client_settings: settings::ResolvedClientSettings,
|
|
document_ref: index::DocumentQuery,
|
|
position_encoding: PositionEncoding,
|
|
}
|
|
|
|
impl Session {
|
|
pub fn new(
|
|
client_capabilities: &ClientCapabilities,
|
|
position_encoding: PositionEncoding,
|
|
global_settings: ClientSettings,
|
|
workspaces: &Workspaces,
|
|
) -> crate::Result<Self> {
|
|
Ok(Self {
|
|
position_encoding,
|
|
index: index::Index::new(workspaces, &global_settings)?,
|
|
global_settings,
|
|
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
|
client_capabilities,
|
|
)),
|
|
})
|
|
}
|
|
|
|
pub fn key_from_url(&self, url: Url) -> DocumentKey {
|
|
self.index.key_from_url(url)
|
|
}
|
|
|
|
/// Creates a document snapshot with the URL referencing the document to snapshot.
|
|
pub fn take_snapshot(&self, url: Url) -> Option<DocumentSnapshot> {
|
|
let key = self.key_from_url(url);
|
|
Some(DocumentSnapshot {
|
|
resolved_client_capabilities: self.resolved_client_capabilities.clone(),
|
|
client_settings: self.index.client_settings(&key, &self.global_settings),
|
|
document_ref: self.index.make_document_ref(key, &self.global_settings)?,
|
|
position_encoding: self.position_encoding,
|
|
})
|
|
}
|
|
|
|
/// Iterates over the LSP URLs for all open text documents. These URLs are valid file paths.
|
|
pub(super) fn text_document_urls(&self) -> impl Iterator<Item = &lsp_types::Url> + '_ {
|
|
self.index.text_document_urls()
|
|
}
|
|
|
|
/// Iterates over the LSP URLs for all open notebook documents. These URLs are valid file paths.
|
|
pub(super) fn notebook_document_urls(&self) -> impl Iterator<Item = &lsp_types::Url> + '_ {
|
|
self.index.notebook_document_urls()
|
|
}
|
|
|
|
/// Updates a text document at the associated `key`.
|
|
///
|
|
/// The document key must point to a text document, or this will throw an error.
|
|
pub(crate) fn update_text_document(
|
|
&mut self,
|
|
key: &DocumentKey,
|
|
content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
|
|
new_version: DocumentVersion,
|
|
) -> crate::Result<()> {
|
|
let encoding = self.encoding();
|
|
|
|
self.index
|
|
.update_text_document(key, content_changes, new_version, encoding)
|
|
}
|
|
|
|
/// Updates a notebook document at the associated `key` with potentially new
|
|
/// cell, metadata, and version values.
|
|
///
|
|
/// The document key must point to a notebook document or cell, or this will
|
|
/// throw an error.
|
|
pub fn update_notebook_document(
|
|
&mut self,
|
|
key: &DocumentKey,
|
|
cells: Option<NotebookDocumentCellChange>,
|
|
metadata: Option<serde_json::Map<String, serde_json::Value>>,
|
|
version: DocumentVersion,
|
|
) -> crate::Result<()> {
|
|
let encoding = self.encoding();
|
|
self.index
|
|
.update_notebook_document(key, cells, metadata, version, encoding)
|
|
}
|
|
|
|
/// Registers a notebook document at the provided `url`.
|
|
/// If a document is already open here, it will be overwritten.
|
|
pub fn open_notebook_document(&mut self, url: Url, document: NotebookDocument) {
|
|
self.index.open_notebook_document(url, document);
|
|
}
|
|
|
|
/// Registers a text document at the provided `url`.
|
|
/// If a document is already open here, it will be overwritten.
|
|
pub(crate) fn open_text_document(&mut self, url: Url, document: TextDocument) {
|
|
self.index.open_text_document(url, document);
|
|
}
|
|
|
|
/// De-registers a document, specified by its key.
|
|
/// Calling this multiple times for the same document is a logic error.
|
|
pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
|
|
self.index.close_document(key)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reloads the settings index
|
|
pub(crate) fn reload_settings(&mut self, changed_url: &Url) {
|
|
self.index.reload_settings(changed_url);
|
|
}
|
|
|
|
/// Open a workspace folder at the given `url`.
|
|
pub(crate) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> {
|
|
self.index.open_workspace_folder(url, &self.global_settings)
|
|
}
|
|
|
|
/// Close a workspace folder at the given `url`.
|
|
pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> {
|
|
self.index.close_workspace_folder(url)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn num_documents(&self) -> usize {
|
|
self.index.num_documents()
|
|
}
|
|
|
|
pub(crate) fn num_workspaces(&self) -> usize {
|
|
self.index.num_workspaces()
|
|
}
|
|
|
|
pub(crate) fn list_config_files(&self) -> Vec<&std::path::Path> {
|
|
self.index.list_config_files()
|
|
}
|
|
|
|
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
|
|
&self.resolved_client_capabilities
|
|
}
|
|
|
|
pub(crate) fn encoding(&self) -> PositionEncoding {
|
|
self.position_encoding
|
|
}
|
|
}
|
|
|
|
impl DocumentSnapshot {
|
|
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
|
|
&self.resolved_client_capabilities
|
|
}
|
|
|
|
pub(crate) fn client_settings(&self) -> &settings::ResolvedClientSettings {
|
|
&self.client_settings
|
|
}
|
|
|
|
pub fn query(&self) -> &index::DocumentQuery {
|
|
&self.document_ref
|
|
}
|
|
|
|
pub(crate) fn encoding(&self) -> PositionEncoding {
|
|
self.position_encoding
|
|
}
|
|
}
|