[ty] Request configuration from client (#18984)
Some checks are pending
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

This PR makes the necessary changes to the server that it can request
configurations from the client using the `configuration` request.
This PR doesn't make use of the request yet. It only sets up the
foundation (mainly the coordination between client and server)
so that future PRs could pull specific settings. 

I plan to use this for pulling the Python environment from the Python
extension.

Deno does something very similar to this.

## Test Plan

Tested that diagnostics are still shown.
This commit is contained in:
Micha Reiser 2025-07-02 11:01:41 +02:00 committed by GitHub
parent cdf91b8b74
commit f7fc8fb084
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 450 additions and 187 deletions

View file

@ -263,12 +263,23 @@ impl Files {
impl fmt::Debug for Files {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut map = f.debug_map();
if f.alternate() {
let mut map = f.debug_map();
for entry in self.inner.system_by_path.iter() {
map.entry(entry.key(), entry.value());
for entry in self.inner.system_by_path.iter() {
map.entry(entry.key(), entry.value());
}
map.finish()
} else {
f.debug_struct("Files")
.field("system_by_path", &self.inner.system_by_path.len())
.field(
"system_virtual_by_path",
&self.inner.system_virtual_by_path.len(),
)
.field("vendored_by_path", &self.inner.vendored_by_path.len())
.finish()
}
map.finish()
}
}

View file

@ -1,3 +1,4 @@
use std::fmt::Formatter;
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
use std::sync::Arc;
use std::{cmp, fmt};
@ -146,6 +147,16 @@ impl ProjectDatabase {
}
}
impl std::fmt::Debug for ProjectDatabase {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ProjectDatabase")
.field("project", &self.project)
.field("files", &self.files)
.field("system", &self.system)
.finish_non_exhaustive()
}
}
/// Stores memory usage information.
pub struct SalsaMemoryDump {
total_fields: usize,

View file

@ -55,6 +55,7 @@ pub fn default_lints_registry() -> LintRegistry {
/// it remains the same project. That's why program is a narrowed view of the project only
/// holding on to the most fundamental settings required for checking.
#[salsa::input]
#[derive(Debug)]
pub struct Project {
/// The files that are open in the project.
///

View file

@ -136,7 +136,7 @@ impl Server {
&client_capabilities,
position_encoding,
global_options,
&workspaces,
workspaces,
)?,
client_capabilities,
})
@ -227,12 +227,10 @@ impl ServerPanicHookHandler {
writeln!(stderr, "{panic_info}\n{backtrace}").ok();
if let Some(client) = hook_client.upgrade() {
client
.show_message(
"The ty language server exited with a panic. See the logs for more details.",
MessageType::ERROR,
)
.ok();
client.show_message(
"The ty language server exited with a panic. See the logs for more details.",
MessageType::ERROR,
);
}
}));

View file

@ -160,7 +160,7 @@ where
};
let db = match &path {
AnySystemPath::System(path) => match session.project_db_for_path(path.as_std_path()) {
AnySystemPath::System(path) => match session.project_db_for_path(path) {
Some(db) => db.clone(),
None => session.default_project_db().clone(),
},
@ -224,17 +224,14 @@ where
request.id,
request.method
);
if client.retry(request).is_ok() {
return None;
}
client.retry(request);
} else {
tracing::trace!(
"request id={} was cancelled by salsa, sending content modified",
id
);
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
}
tracing::trace!(
"request id={} was cancelled by salsa, sending content modified",
id
);
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
None
} else {
Some(Err(Error {
@ -343,17 +340,13 @@ fn respond<Req>(
tracing::error!("An error occurred with request ID {id}: {err}");
client.show_error_message("ty encountered a problem. Check the logs for more details.");
}
if let Err(err) = client.respond(id, result) {
tracing::error!("Failed to send response: {err}");
}
client.respond(id, result);
}
/// Sends back an error response to the server using a [`Client`] without showing a warning
/// to the user.
fn respond_silent_error(id: RequestId, client: &Client, error: lsp_server::ResponseError) {
if let Err(err) = client.respond_err(id, error) {
tracing::error!("Failed to send response: {err}");
}
client.respond_err(id, error);
}
/// Tries to cast a serialized request from the server into

View file

@ -1,4 +1,3 @@
use lsp_server::ErrorCode;
use lsp_types::notification::PublishDiagnostics;
use lsp_types::{
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
@ -46,20 +45,17 @@ impl Diagnostics {
/// This is done by notifying the client with an empty list of diagnostics for the document.
/// For notebook cells, this clears diagnostics for the specific cell.
/// For other document types, this clears diagnostics for the main document.
pub(super) fn clear_diagnostics(key: &DocumentKey, client: &Client) -> Result<()> {
pub(super) fn clear_diagnostics(key: &DocumentKey, client: &Client) {
let Some(uri) = key.to_url() else {
// If we can't convert to URL, we can't clear diagnostics
return Ok(());
return;
};
client
.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
uri,
diagnostics: vec![],
version: None,
})
.with_failure_code(ErrorCode::InternalError)?;
Ok(())
client.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
uri,
diagnostics: vec![],
version: None,
});
}
/// Publishes the diagnostics for the given document snapshot using the [publish diagnostics
@ -96,22 +92,20 @@ pub(super) fn publish_diagnostics(
// Sends a notification to the client with the diagnostics for the document.
let publish_diagnostics_notification = |uri: Url, diagnostics: Vec<Diagnostic>| {
client
.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
uri,
diagnostics,
version: Some(snapshot.query().version()),
})
.with_failure_code(lsp_server::ErrorCode::InternalError)
client.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
uri,
diagnostics,
version: Some(snapshot.query().version()),
});
};
match diagnostics {
Diagnostics::TextDocument(diagnostics) => {
publish_diagnostics_notification(url, diagnostics)?;
publish_diagnostics_notification(url, diagnostics);
}
Diagnostics::NotebookDocument(cell_diagnostics) => {
for (cell_url, diagnostics) in cell_diagnostics {
publish_diagnostics_notification(cell_url, diagnostics)?;
publish_diagnostics_notification(cell_url, diagnostics);
}
}
}

View file

@ -20,7 +20,7 @@ impl SyncNotificationHandler for CancelNotificationHandler {
lsp_types::NumberOrString::String(id) => id.into(),
};
let _ = client.cancel(session, id);
client.cancel(session, id);
Ok(())
}

View file

@ -39,7 +39,7 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
match key.path() {
AnySystemPath::System(path) => {
let db = match session.project_db_for_path_mut(path.as_std_path()) {
let db = match session.project_db_for_path_mut(path) {
Some(db) => db,
None => session.default_project_db_mut(),
};

View file

@ -1,5 +1,4 @@
use crate::server::Result;
use crate::server::api::LSPResult;
use crate::server::api::diagnostics::publish_diagnostics;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session;
@ -45,7 +44,7 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
}
};
let Some(db) = session.project_db_for_path(system_path.as_std_path()) else {
let Some(db) = session.project_db_for_path(&system_path) else {
tracing::trace!(
"Ignoring change event for `{system_path}` because it's not in any workspace"
);
@ -103,13 +102,11 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
if project_changed {
if client_capabilities.diagnostics_refresh {
client
.send_request::<types::request::WorkspaceDiagnosticRefresh>(
session,
(),
|_, ()| {},
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
client.send_request::<types::request::WorkspaceDiagnosticRefresh>(
session,
(),
|_, ()| {},
);
} else {
for key in session.text_document_keys() {
publish_diagnostics(session, &key, client)?;
@ -120,9 +117,7 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
}
if client_capabilities.inlay_refresh {
client
.send_request::<types::request::InlayHintRefreshRequest>(session, (), |_, ()| {})
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
client.send_request::<types::request::InlayHintRefreshRequest>(session, (), |_, ()| {});
}
Ok(())

View file

@ -41,6 +41,8 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
);
}
clear_diagnostics(&key, client)
clear_diagnostics(&key, client);
Ok(())
}
}

View file

@ -41,7 +41,7 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
match key.path() {
AnySystemPath::System(system_path) => {
let db = match session.project_db_for_path_mut(system_path.as_std_path()) {
let db = match session.project_db_for_path_mut(system_path) {
Some(db) => db,
None => session.default_project_db_mut(),
};

View file

@ -40,7 +40,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
match &path {
AnySystemPath::System(system_path) => {
let db = match session.project_db_for_path_mut(system_path.as_std_path()) {
let db = match session.project_db_for_path_mut(system_path) {
Some(db) => db,
None => session.default_project_db_mut(),
};

View file

@ -1,11 +1,15 @@
use crate::server::schedule::Scheduler;
use crate::server::{Server, api};
use crate::session::ClientOptions;
use crate::session::client::Client;
use anyhow::anyhow;
use crossbeam::select;
use lsp_server::Message;
use lsp_types::notification::Notification;
use lsp_types::{DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher};
use lsp_types::{
ConfigurationParams, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Url,
};
use serde_json::Value;
pub(crate) type MainLoopSender = crossbeam::channel::Sender<Event>;
pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver<Event>;
@ -26,6 +30,10 @@ impl Server {
match next_event {
Event::Message(msg) => {
let Some(msg) = self.session.should_defer_message(msg) else {
continue;
};
let client = Client::new(
self.main_loop_sender.clone(),
self.connection.sender.clone(),
@ -49,7 +57,7 @@ impl Server {
message: "Shutdown already requested".to_owned(),
data: None,
},
)?;
);
continue;
}
@ -130,6 +138,9 @@ impl Server {
);
}
}
Action::InitializeWorkspaces(workspaces_with_options) => {
self.session.initialize_workspaces(workspaces_with_options);
}
},
}
}
@ -140,7 +151,25 @@ impl Server {
/// Waits for the next message from the client or action.
///
/// Returns `Ok(None)` if the client connection is closed.
fn next_event(&self) -> Result<Option<Event>, crossbeam::channel::RecvError> {
fn next_event(&mut self) -> Result<Option<Event>, crossbeam::channel::RecvError> {
// We can't queue those into the main loop because that could result in reordering if
// the `select` below picks a client message first.
if let Some(deferred) = self.session.take_deferred_messages() {
match &deferred {
Message::Request(req) => {
tracing::debug!("Processing deferred request `{}`", req.method);
}
Message::Notification(notification) => {
tracing::debug!("Processing deferred notification `{}`", notification.method);
}
Message::Response(response) => {
tracing::debug!("Processing deferred response `{}`", response.id);
}
}
return Ok(Some(Event::Message(deferred)));
}
select!(
recv(self.connection.receiver) -> msg => {
// Ignore disconnect errors, they're handled by the main loop (it will exit).
@ -151,6 +180,47 @@ impl Server {
}
fn initialize(&mut self, client: &Client) {
let urls = self
.session
.workspaces()
.urls()
.cloned()
.collect::<Vec<_>>();
let items = urls
.iter()
.map(|root| lsp_types::ConfigurationItem {
scope_uri: Some(root.clone()),
section: Some("ty".to_string()),
})
.collect();
tracing::debug!("Requesting workspace configuration for workspaces");
client
.send_request::<lsp_types::request::WorkspaceConfiguration>(
&self.session,
ConfigurationParams { items },
|client, result: Vec<Value>| {
tracing::debug!("Received workspace configurations, initializing workspaces");
assert_eq!(result.len(), urls.len());
let workspaces_with_options: Vec<_> = urls
.into_iter()
.zip(result)
.map(|(url, value)| {
let options: ClientOptions = serde_json::from_value(value).unwrap_or_else(|err| {
tracing::warn!("Failed to deserialize workspace options for {url}: {err}. Using default options.");
ClientOptions::default()
});
(url, options)
})
.collect();
client.queue_action(Action::InitializeWorkspaces(workspaces_with_options));
},
);
let fs_watcher = self
.client_capabilities
.workspace
@ -206,17 +276,13 @@ impl Server {
tracing::info!("File watcher successfully registered");
};
if let Err(err) = client.send_request::<lsp_types::request::RegisterCapability>(
client.send_request::<lsp_types::request::RegisterCapability>(
&self.session,
lsp_types::RegistrationParams {
registrations: vec![registration],
},
response_handler,
) {
tracing::error!(
"An error occurred when trying to register the configuration file watcher: {err}"
);
}
);
} else {
tracing::warn!("The client does not support file system watching.");
}
@ -231,6 +297,10 @@ pub(crate) enum Action {
/// Retry a request that previously failed due to a salsa cancellation.
RetryRequest(lsp_server::Request),
/// Initialize the workspace after the server received
/// the options from the client.
InitializeWorkspaces(Vec<(Url, ClientOptions)>),
}
#[derive(Debug)]

View file

@ -83,9 +83,7 @@ impl Task {
R: Serialize + Send + 'static,
{
Self::sync(move |_, client| {
if let Err(err) = client.respond(&id, result) {
tracing::error!("Unable to send immediate response: {err}");
}
client.respond(&id, result);
})
}

View file

@ -1,22 +1,24 @@
//! Data model, state management, and configuration resolution.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::{Context, anyhow};
use lsp_server::Message;
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url};
use options::GlobalOptions;
use ruff_db::Db;
use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::SystemPath;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ty_project::metadata::Options;
use ty_project::{ProjectDatabase, ProjectMetadata};
pub(crate) use self::capabilities::ResolvedClientCapabilities;
pub use self::index::DocumentQuery;
pub(crate) use self::options::{AllOptions, ClientOptions};
use self::settings::ClientSettings;
pub(crate) use self::settings::ClientSettings;
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
use crate::session::request_queue::RequestQueue;
use crate::system::{AnySystemPath, LSPSystem};
@ -40,8 +42,13 @@ pub struct Session {
/// [`index_mut`]: Session::index_mut
index: Option<Arc<index::Index>>,
/// Maps workspace folders to their respective project databases.
projects_by_workspace_folder: BTreeMap<PathBuf, ProjectDatabase>,
/// Maps workspace folders to their respective workspace.
workspaces: Workspaces,
/// The projects across all workspaces.
projects: BTreeMap<SystemPathBuf, ProjectDatabase>,
default_project: ProjectDatabase,
/// The global position encoding, negotiated during LSP initialization.
position_encoding: PositionEncoding,
@ -54,6 +61,8 @@ pub struct Session {
/// Has the client requested the server to shutdown.
shutdown_requested: bool,
deferred_messages: VecDeque<Message>,
}
impl Session {
@ -61,32 +70,33 @@ impl Session {
client_capabilities: &ClientCapabilities,
position_encoding: PositionEncoding,
global_options: GlobalOptions,
workspace_folders: &[(Url, ClientOptions)],
workspace_folders: Vec<(Url, ClientOptions)>,
) -> crate::Result<Self> {
let mut workspaces = BTreeMap::new();
let index = Arc::new(index::Index::new(global_options.into_settings()));
// TODO: Consider workspace settings
for (url, _) in workspace_folders {
let path = url
.to_file_path()
.map_err(|()| anyhow!("Workspace URL is not a file or directory: {:?}", url))?;
let system_path = SystemPath::from_std_path(&path)
.ok_or_else(|| anyhow!("Workspace path is not a valid UTF-8 path: {:?}", path))?;
let system = LSPSystem::new(index.clone());
// TODO(dhruvmanila): Get the values from the client settings
let mut metadata = ProjectMetadata::discover(system_path, &system)?;
metadata.apply_configuration_files(&system)?;
// TODO(micha): Handle the case where the program settings are incorrect more gracefully.
workspaces.insert(path, ProjectDatabase::new(metadata, system)?);
let mut workspaces = Workspaces::default();
for (url, options) in workspace_folders {
workspaces.register(url, options)?;
}
let default_project = {
let system = LSPSystem::new(index.clone());
let metadata = ProjectMetadata::from_options(
Options::default(),
system.current_directory().to_path_buf(),
None,
)
.unwrap();
ProjectDatabase::new(metadata, system).unwrap()
};
Ok(Self {
position_encoding,
projects_by_workspace_folder: workspaces,
workspaces,
deferred_messages: VecDeque::new(),
index: Some(index),
default_project,
projects: BTreeMap::new(),
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
client_capabilities,
)),
@ -111,6 +121,52 @@ impl Session {
self.shutdown_requested = requested;
}
/// The LSP specification doesn't allow configuration requests during initialization,
/// but we need access to the configuration to resolve the settings in turn to create the
/// project databases. This will become more important in the future when we support
/// persistent caching. It's then crucial that we have the correct settings to select the
/// right cache.
///
/// We work around this by queueing up all messages that arrive between the `initialized` notification
/// and the completion of workspace initialization (which waits for the client's configuration response).
///
/// This queuing is only necessary when registering *new* workspaces. Changes to configurations
/// don't need to go through the same process because we can update the existing
/// database in place.
///
/// See <https://github.com/Microsoft/language-server-protocol/issues/567#issuecomment-2085131917>
pub(crate) fn should_defer_message(&mut self, message: Message) -> Option<Message> {
if self.workspaces.all_initialized() {
Some(message)
} else {
match &message {
Message::Request(request) => {
tracing::debug!(
"Deferring `{}` request until all workspaces are initialized",
request.method
);
}
Message::Response(_) => {
// We still want to get client responses even during workspace initialization.
return Some(message);
}
Message::Notification(notification) => {
tracing::debug!(
"Deferring `{}` notification until all workspaces are initialized",
notification.method
);
}
}
self.deferred_messages.push_back(message);
None
}
}
pub(crate) fn workspaces(&self) -> &Workspaces {
&self.workspaces
}
// TODO(dhruvmanila): Ideally, we should have a single method for `workspace_db_for_path_mut`
// and `default_workspace_db_mut` but the borrow checker doesn't allow that.
// https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437
@ -119,14 +175,17 @@ impl Session {
/// or the default project if no project is found for the path.
pub(crate) fn project_db_or_default(&self, path: &AnySystemPath) -> &ProjectDatabase {
path.as_system()
.and_then(|path| self.project_db_for_path(path.as_std_path()))
.and_then(|path| self.project_db_for_path(path))
.unwrap_or_else(|| self.default_project_db())
}
/// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if
/// any.
pub(crate) fn project_db_for_path(&self, path: impl AsRef<Path>) -> Option<&ProjectDatabase> {
self.projects_by_workspace_folder
pub(crate) fn project_db_for_path(
&self,
path: impl AsRef<SystemPath>,
) -> Option<&ProjectDatabase> {
self.projects
.range(..=path.as_ref().to_path_buf())
.next_back()
.map(|(_, db)| db)
@ -136,9 +195,9 @@ impl Session {
/// path, if any.
pub(crate) fn project_db_for_path_mut(
&mut self,
path: impl AsRef<Path>,
path: impl AsRef<SystemPath>,
) -> Option<&mut ProjectDatabase> {
self.projects_by_workspace_folder
self.projects
.range_mut(..=path.as_ref().to_path_buf())
.next_back()
.map(|(_, db)| db)
@ -147,23 +206,85 @@ impl Session {
/// Returns a reference to the default project [`ProjectDatabase`]. The default project is the
/// minimum root path in the project map.
pub(crate) fn default_project_db(&self) -> &ProjectDatabase {
// SAFETY: Currently, ty only support a single project.
self.projects_by_workspace_folder.values().next().unwrap()
&self.default_project
}
/// Returns a mutable reference to the default project [`ProjectDatabase`].
pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase {
// SAFETY: Currently, ty only support a single project.
self.projects_by_workspace_folder
&mut self.default_project
}
fn projects_mut(&mut self) -> impl Iterator<Item = &'_ mut ProjectDatabase> + '_ {
self.projects
.values_mut()
.next()
.unwrap()
.chain(std::iter::once(&mut self.default_project))
}
pub(crate) fn key_from_url(&self, url: Url) -> crate::Result<DocumentKey> {
self.index().key_from_url(url)
}
pub(crate) fn initialize_workspaces(&mut self, workspace_settings: Vec<(Url, ClientOptions)>) {
assert!(!self.workspaces.all_initialized());
for (url, options) in workspace_settings {
let Some(workspace) = self.workspaces.initialize(&url, options) else {
continue;
};
// For now, create one project database per workspace.
// In the future, index the workspace directories to find all projects
// and create a project database for each.
let system = LSPSystem::new(self.index.as_ref().unwrap().clone());
let Some(system_path) = SystemPath::from_std_path(workspace.root()) else {
tracing::warn!(
"Ignore workspace `{}` because it's root contains non UTF8 characters",
workspace.root().display()
);
continue;
};
let root = system_path.to_path_buf();
let project = ProjectMetadata::discover(&root, &system)
.context("Failed to find project configuration")
.and_then(|mut metadata| {
// TODO(dhruvmanila): Merge the client options with the project metadata options.
metadata
.apply_configuration_files(&system)
.context("Failed to apply configuration files")?;
ProjectDatabase::new(metadata, system)
.context("Failed to create project database")
});
// TODO(micha): Handle the case where the program settings are incorrect more gracefully.
// The easiest is to ignore those projects but to show a message to the user that we do so.
// Ignoring the projects has the effect that we'll use the default project for those files.
// The only challenge with this is that we need to register the project when the configuration
// becomes valid again. But that's a case we need to handle anyway for good mono repository support.
match project {
Ok(project) => {
self.projects.insert(root, project);
}
Err(err) => {
tracing::warn!("Failed to create project database for `{root}`: {err}",);
}
}
}
assert!(
self.workspaces.all_initialized(),
"All workspaces should be initialized after calling `initialize_workspaces`"
);
}
pub(crate) fn take_deferred_messages(&mut self) -> Option<Message> {
if self.workspaces.all_initialized() {
self.deferred_messages.pop_front()
} else {
None
}
}
/// Creates a document snapshot with the URL referencing the document to snapshot.
///
/// Returns `None` if the url can't be converted to a document key or if the document isn't open.
@ -240,7 +361,7 @@ impl Session {
fn index_mut(&mut self) -> MutIndexGuard {
let index = self.index.take().unwrap();
for db in self.projects_by_workspace_folder.values_mut() {
for db in self.projects_mut() {
// Remove the `index` from each database. This drops the count of `Arc<Index>` down to 1
db.system_mut()
.as_any_mut()
@ -250,11 +371,11 @@ impl Session {
}
// There should now be exactly one reference to index which is self.index.
let index = Arc::into_inner(index);
let index = Arc::into_inner(index).unwrap();
MutIndexGuard {
session: self,
index,
index: Some(index),
}
}
@ -289,7 +410,7 @@ impl Drop for MutIndexGuard<'_> {
fn drop(&mut self) {
if let Some(index) = self.index.take() {
let index = Arc::new(index);
for db in self.session.projects_by_workspace_folder.values_mut() {
for db in self.session.projects_mut() {
db.system_mut()
.as_any_mut()
.downcast_mut::<LSPSystem>()
@ -339,3 +460,72 @@ impl DocumentSnapshot {
}
}
}
#[derive(Debug, Default)]
pub(crate) struct Workspaces {
workspaces: BTreeMap<Url, Workspace>,
uninitialized: usize,
}
impl Workspaces {
pub(crate) fn register(&mut self, url: Url, options: ClientOptions) -> anyhow::Result<()> {
let path = url
.to_file_path()
.map_err(|()| anyhow!("Workspace URL is not a file or directory: {url:?}"))?;
self.workspaces.insert(
url,
Workspace {
options,
root: path,
},
);
self.uninitialized += 1;
Ok(())
}
pub(crate) fn initialize(
&mut self,
url: &Url,
options: ClientOptions,
) -> Option<&mut Workspace> {
if let Some(workspace) = self.workspaces.get_mut(url) {
workspace.options = options;
self.uninitialized -= 1;
Some(workspace)
} else {
None
}
}
pub(crate) fn urls(&self) -> impl Iterator<Item = &Url> + '_ {
self.workspaces.keys()
}
pub(crate) fn all_initialized(&self) -> bool {
self.uninitialized == 0
}
}
impl<'a> IntoIterator for &'a Workspaces {
type Item = (&'a Url, &'a Workspace);
type IntoIter = std::collections::btree_map::Iter<'a, Url, Workspace>;
fn into_iter(self) -> Self::IntoIter {
self.workspaces.iter()
}
}
#[derive(Debug)]
pub(crate) struct Workspace {
root: PathBuf,
options: ClientOptions,
}
impl Workspace {
pub(crate) fn root(&self) -> &Path {
&self.root
}
}

View file

@ -1,7 +1,6 @@
use crate::Session;
use crate::server::{Action, ConnectionSender};
use crate::server::{Event, MainLoopSender};
use anyhow::{Context, anyhow};
use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError};
use serde_json::Value;
use std::any::TypeId;
@ -45,8 +44,7 @@ impl Client {
session: &Session,
params: R::Params,
response_handler: impl FnOnce(&Client, R::Result) + Send + 'static,
) -> crate::Result<()>
where
) where
R: lsp_types::request::Request,
{
let response_handler = Box::new(move |client: &Client, response: lsp_server::Response| {
@ -95,60 +93,64 @@ impl Client {
.outgoing()
.register(response_handler);
self.client_sender
if let Err(err) = self
.client_sender
.send(Message::Request(lsp_server::Request {
id,
method: R::METHOD.to_string(),
params: serde_json::to_value(params).context("Failed to serialize params")?,
params: serde_json::to_value(params).expect("Params to be serializable"),
}))
.with_context(|| {
format!("Failed to send request method={method}", method = R::METHOD)
})?;
Ok(())
{
tracing::error!(
"Failed to send request `{}` because the client sender is closed: {err}",
R::METHOD
);
}
}
/// Sends a notification to the client.
pub(crate) fn send_notification<N>(&self, params: N::Params) -> crate::Result<()>
pub(crate) fn send_notification<N>(&self, params: N::Params)
where
N: lsp_types::notification::Notification,
{
let method = N::METHOD.to_string();
self.client_sender
.send(lsp_server::Message::Notification(Notification::new(
method, params,
)))
.map_err(|error| {
anyhow!(
"Failed to send notification (method={method}): {error}",
method = N::METHOD
)
})
if let Err(err) =
self.client_sender
.send(lsp_server::Message::Notification(Notification::new(
method, params,
)))
{
tracing::error!(
"Failed to send notification `{}` because the client sender is closed: {err}",
N::METHOD
);
}
}
/// Sends a notification without any parameters to the client.
///
/// This is useful for notifications that don't require any data.
#[expect(dead_code)]
pub(crate) fn send_notification_no_params(&self, method: &str) -> crate::Result<()> {
self.client_sender
.send(lsp_server::Message::Notification(Notification::new(
method.to_string(),
Value::Null,
)))
.map_err(|error| anyhow!("Failed to send notification (method={method}): {error}",))
pub(crate) fn send_notification_no_params(&self, method: &str) {
if let Err(err) =
self.client_sender
.send(lsp_server::Message::Notification(Notification::new(
method.to_string(),
Value::Null,
)))
{
tracing::error!(
"Failed to send notification `{method}` because the client sender is closed: {err}",
);
}
}
/// Sends a response to the client for a given request ID.
///
/// The response isn't sent immediately. Instead, it's queued up in the main loop
/// and checked for cancellation (each request must have exactly one response).
pub(crate) fn respond<R>(
&self,
id: &RequestId,
result: crate::server::Result<R>,
) -> crate::Result<()>
pub(crate) fn respond<R>(&self, id: &RequestId, result: crate::server::Result<R>)
where
R: serde::Serialize,
{
@ -161,17 +163,13 @@ impl Client {
self.main_loop_sender
.send(Event::Action(Action::SendResponse(response)))
.map_err(|error| anyhow!("Failed to send response for request {id}: {error}"))
.unwrap();
}
/// Sends an error response to the client for a given request ID.
///
/// The response isn't sent immediately. Instead, it's queued up in the main loop.
pub(crate) fn respond_err(
&self,
id: RequestId,
error: lsp_server::ResponseError,
) -> crate::Result<()> {
pub(crate) fn respond_err(&self, id: RequestId, error: lsp_server::ResponseError) {
let response = lsp_server::Response {
id,
result: None,
@ -180,23 +178,19 @@ impl Client {
self.main_loop_sender
.send(Event::Action(Action::SendResponse(response)))
.map_err(|error| anyhow!("Failed to send response: {error}"))
.unwrap();
}
/// Shows a message to the user.
///
/// This opens a pop up in VS Code showing `message`.
pub(crate) fn show_message(
&self,
message: impl Display,
message_type: lsp_types::MessageType,
) -> crate::Result<()> {
pub(crate) fn show_message(&self, message: impl Display, message_type: lsp_types::MessageType) {
self.send_notification::<lsp_types::notification::ShowMessage>(
lsp_types::ShowMessageParams {
typ: message_type,
message: message.to_string(),
},
)
);
}
/// Sends a request to display a warning to the client with a formatted message. The warning is
@ -204,11 +198,7 @@ impl Client {
///
/// Logs an error if the message could not be sent.
pub(crate) fn show_warning_message(&self, message: impl Display) {
let result = self.show_message(message, lsp_types::MessageType::WARNING);
if let Err(err) = result {
tracing::error!("Failed to send warning message to the client: {err}");
}
self.show_message(message, lsp_types::MessageType::WARNING);
}
/// Sends a request to display an error to the client with a formatted message. The error is
@ -216,23 +206,23 @@ impl Client {
///
/// Logs an error if the message could not be sent.
pub(crate) fn show_error_message(&self, message: impl Display) {
let result = self.show_message(message, lsp_types::MessageType::ERROR);
if let Err(err) = result {
tracing::error!("Failed to send error message to the client: {err}");
}
self.show_message(message, lsp_types::MessageType::ERROR);
}
/// Re-queues this request after a salsa cancellation for a retry.
///
/// The main loop will skip the retry if the client cancelled the request in the meantime.
pub(crate) fn retry(&self, request: lsp_server::Request) -> crate::Result<()> {
pub(crate) fn retry(&self, request: lsp_server::Request) {
self.main_loop_sender
.send(Event::Action(Action::RetryRequest(request)))
.map_err(|error| anyhow!("Failed to send retry request: {error}"))
.unwrap();
}
pub(crate) fn cancel(&self, session: &mut Session, id: RequestId) -> crate::Result<()> {
pub(crate) fn queue_action(&self, action: Action) {
self.main_loop_sender.send(Event::Action(action)).unwrap();
}
pub(crate) fn cancel(&self, session: &mut Session, id: RequestId) {
let method_name = session.request_queue_mut().incoming_mut().cancel(&id);
if let Some(method_name) = method_name {
@ -245,14 +235,18 @@ impl Client {
// Use `client_sender` here instead of `respond_err` because
// `respond_err` filters out responses for canceled requests (which we just did!).
self.client_sender
if let Err(err) = self
.client_sender
.send(Message::Response(lsp_server::Response {
id,
result: None,
error: Some(error),
}))?;
}))
{
tracing::error!(
"Failed to send cancellation response for request `{method_name}` because the client sender is closed: {err}",
);
}
}
Ok(())
}
}

View file

@ -24,14 +24,7 @@ pub(crate) struct GlobalOptions {
impl GlobalOptions {
pub(crate) fn into_settings(self) -> ClientSettings {
ClientSettings {
disable_language_services: self
.client
.python
.and_then(|python| python.ty)
.and_then(|ty| ty.disable_language_services)
.unwrap_or_default(),
}
self.client.into_settings()
}
}
@ -56,6 +49,19 @@ pub(crate) struct ClientOptions {
python: Option<Python>,
}
impl ClientOptions {
/// Returns the client settings that are relevant to the language server.
pub(crate) fn into_settings(self) -> ClientSettings {
ClientSettings {
disable_language_services: self
.python
.and_then(|python| python.ty)
.and_then(|ty| ty.disable_language_services)
.unwrap_or_default(),
}
}
}
// TODO(dhruvmanila): We need to mirror the "python.*" namespace on the server side but ideally it
// would be useful to instead use `workspace/configuration` instead. This would be then used to get
// all settings and not just the ones in "python.*".