ruff/crates/ruff_server/src/session.rs
Dhruv Manilawala b5dbb2a1d7
Avoid indexing the same workspace multiple times (#15495)
## Summary

This is not lazy indexing but it should somewhat help with #13686.

Currently, processing the change notifications for config files doesn't
account for the fact that multiple config files could belong to the same
workspace. This means that the server will re-index the same workspace
`n` times where `n` is the number of file events which belongs to the
same workspace. This is evident in the following trace logs:

**Trace logs:**

```
[Trace - 6:21:15 PM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
    "changes": [
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/pylint/ruff.toml",
            "type": 1
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/script/ruff.toml",
            "type": 2
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/script/scaffold/templates/ruff.toml",
            "type": 2
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/pyproject.toml",
            "type": 2
        }
    ]
}

...

[Trace - 6:21:19 PM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
    "changes": [
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/tests/testing_config/custom_components/ruff.toml",
            "type": 1
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/tests/ruff.toml",
            "type": 2
        }
    ]
}

...
```

**Server logs:**

```
 14.838004208s TRACE     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::server::api: enter
  14.838043583s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  14.854324541s DEBUG ThreadId(55) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  14.854388500s DEBUG ThreadId(55) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  14.937713291s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  14.954429833s DEBUG ThreadId(75) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  14.954675708s DEBUG ThreadId(66) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  15.041465500s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  15.056731541s DEBUG ThreadId(78) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  15.056796833s DEBUG ThreadId(78) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  15.117545833s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  15.133091666s DEBUG ThreadId(90) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  15.133146500s DEBUG ThreadId(90) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  15.220340666s TRACE ruff:worker:6 request{id=5 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  15.220401458s DEBUG ruff:worker:6 request{id=5 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/homeassistant/bootstrap.py
  18.577521250s TRACE     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::server::api: enter
  18.577561291s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  18.616564583s DEBUG ThreadId(102) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  18.616627291s DEBUG ThreadId(102) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  18.687424250s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  18.704441416s DEBUG ThreadId(114) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  18.704694958s DEBUG ThreadId(121) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  18.769627500s TRACE ruff:worker:4 request{id=6 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  18.769696791s DEBUG ruff:worker:4 request{id=6 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/homeassistant/bootstrap.py
```

This PR updates the logic to consider all the change events at once
keeping track of the workspace path that have been already indexed.

I want to include this in tomorrow's release to check how this change
would affect the linked issue.

## Test Plan

Run the same scenario as above and check the logs to see that the server
isn't re-indexing the same workspace multiple times:

**Trace logs:**

```
[Trace - 6:04:07 PM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
    "changes": [
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/script/ruff.toml",
            "type": 1
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/script/scaffold/templates/ruff.toml",
            "type": 1
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/pylint/ruff.toml",
            "type": 2
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/pyproject.toml",
            "type": 2
        }
    ]
}

...

[Trace - 6:04:11 PM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
    "changes": [
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/tests/testing_config/custom_components/ruff.toml",
            "type": 1
        },
        {
            "uri": "file:///Users/dhruv/work/astral/parser-checkouts/home-assistant-core/tests/ruff.toml",
            "type": 2
        }
    ]
}

...
```

**Server logs:**

```
  17.047706750s TRACE     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::server::api: enter
  17.047747875s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  17.080006083s DEBUG ThreadId(54) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  17.080085708s DEBUG ThreadId(54) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  17.145328791s TRACE ruff:worker:6 request{id=5 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  17.145386166s DEBUG ruff:worker:6 request{id=5 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/homeassistant/bootstrap.py
  20.756845958s TRACE     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::server::api: enter
  20.756923375s DEBUG     ruff:main notification{method="workspace/didChangeWatchedFiles"}: ruff_server::session::index::ruff_settings: Indexing settings for workspace: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core
  20.781733916s DEBUG ThreadId(66) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.vscode
  20.781825875s DEBUG ThreadId(75) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/.git
  20.848340750s TRACE ruff:worker:7 request{id=6 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  20.848408041s DEBUG ruff:worker:7 request{id=6 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/work/astral/parser-checkouts/home-assistant-core/homeassistant/bootstrap.py
```
2025-01-15 18:58:28 +05:30

187 lines
6.5 KiB
Rust

//! Data model, state management, and configuration resolution.
use std::sync::Arc;
use lsp_types::{ClientCapabilities, FileEvent, 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 based on the provided changes.
pub(crate) fn reload_settings(&mut self, changes: &[FileEvent]) {
self.index.reload_settings(changes);
}
/// 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
}
}