mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[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
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:
parent
cdf91b8b74
commit
f7fc8fb084
17 changed files with 450 additions and 187 deletions
|
@ -263,12 +263,23 @@ impl Files {
|
||||||
|
|
||||||
impl fmt::Debug for Files {
|
impl fmt::Debug for Files {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
let mut map = f.debug_map();
|
let mut map = f.debug_map();
|
||||||
|
|
||||||
for entry in self.inner.system_by_path.iter() {
|
for entry in self.inner.system_by_path.iter() {
|
||||||
map.entry(entry.key(), entry.value());
|
map.entry(entry.key(), entry.value());
|
||||||
}
|
}
|
||||||
map.finish()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt::Formatter;
|
||||||
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
|
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{cmp, fmt};
|
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.
|
/// Stores memory usage information.
|
||||||
pub struct SalsaMemoryDump {
|
pub struct SalsaMemoryDump {
|
||||||
total_fields: usize,
|
total_fields: usize,
|
||||||
|
|
|
@ -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
|
/// 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.
|
/// holding on to the most fundamental settings required for checking.
|
||||||
#[salsa::input]
|
#[salsa::input]
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
/// The files that are open in the project.
|
/// The files that are open in the project.
|
||||||
///
|
///
|
||||||
|
|
|
@ -136,7 +136,7 @@ impl Server {
|
||||||
&client_capabilities,
|
&client_capabilities,
|
||||||
position_encoding,
|
position_encoding,
|
||||||
global_options,
|
global_options,
|
||||||
&workspaces,
|
workspaces,
|
||||||
)?,
|
)?,
|
||||||
client_capabilities,
|
client_capabilities,
|
||||||
})
|
})
|
||||||
|
@ -227,12 +227,10 @@ impl ServerPanicHookHandler {
|
||||||
writeln!(stderr, "{panic_info}\n{backtrace}").ok();
|
writeln!(stderr, "{panic_info}\n{backtrace}").ok();
|
||||||
|
|
||||||
if let Some(client) = hook_client.upgrade() {
|
if let Some(client) = hook_client.upgrade() {
|
||||||
client
|
client.show_message(
|
||||||
.show_message(
|
|
||||||
"The ty language server exited with a panic. See the logs for more details.",
|
"The ty language server exited with a panic. See the logs for more details.",
|
||||||
MessageType::ERROR,
|
MessageType::ERROR,
|
||||||
)
|
);
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -160,7 +160,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = match &path {
|
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(),
|
Some(db) => db.clone(),
|
||||||
None => session.default_project_db().clone(),
|
None => session.default_project_db().clone(),
|
||||||
},
|
},
|
||||||
|
@ -224,17 +224,14 @@ where
|
||||||
request.id,
|
request.id,
|
||||||
request.method
|
request.method
|
||||||
);
|
);
|
||||||
if client.retry(request).is_ok() {
|
client.retry(request);
|
||||||
return None;
|
} else {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"request id={} was cancelled by salsa, sending content modified",
|
"request id={} was cancelled by salsa, sending content modified",
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|
||||||
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
|
respond_silent_error(id.clone(), client, R::salsa_cancellation_error());
|
||||||
|
}
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Err(Error {
|
Some(Err(Error {
|
||||||
|
@ -343,17 +340,13 @@ fn respond<Req>(
|
||||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||||
}
|
}
|
||||||
if let Err(err) = client.respond(id, result) {
|
client.respond(id, result);
|
||||||
tracing::error!("Failed to send response: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends back an error response to the server using a [`Client`] without showing a warning
|
/// Sends back an error response to the server using a [`Client`] without showing a warning
|
||||||
/// to the user.
|
/// to the user.
|
||||||
fn respond_silent_error(id: RequestId, client: &Client, error: lsp_server::ResponseError) {
|
fn respond_silent_error(id: RequestId, client: &Client, error: lsp_server::ResponseError) {
|
||||||
if let Err(err) = client.respond_err(id, error) {
|
client.respond_err(id, error);
|
||||||
tracing::error!("Failed to send response: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to cast a serialized request from the server into
|
/// Tries to cast a serialized request from the server into
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use lsp_server::ErrorCode;
|
|
||||||
use lsp_types::notification::PublishDiagnostics;
|
use lsp_types::notification::PublishDiagnostics;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
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.
|
/// 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 notebook cells, this clears diagnostics for the specific cell.
|
||||||
/// For other document types, this clears diagnostics for the main document.
|
/// 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 {
|
let Some(uri) = key.to_url() else {
|
||||||
// If we can't convert to URL, we can't clear diagnostics
|
// If we can't convert to URL, we can't clear diagnostics
|
||||||
return Ok(());
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
client.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
|
||||||
.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
|
|
||||||
uri,
|
uri,
|
||||||
diagnostics: vec![],
|
diagnostics: vec![],
|
||||||
version: None,
|
version: None,
|
||||||
})
|
});
|
||||||
.with_failure_code(ErrorCode::InternalError)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publishes the diagnostics for the given document snapshot using the [publish diagnostics
|
/// 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.
|
// Sends a notification to the client with the diagnostics for the document.
|
||||||
let publish_diagnostics_notification = |uri: Url, diagnostics: Vec<Diagnostic>| {
|
let publish_diagnostics_notification = |uri: Url, diagnostics: Vec<Diagnostic>| {
|
||||||
client
|
client.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
|
||||||
.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
|
|
||||||
uri,
|
uri,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
version: Some(snapshot.query().version()),
|
version: Some(snapshot.query().version()),
|
||||||
})
|
});
|
||||||
.with_failure_code(lsp_server::ErrorCode::InternalError)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match diagnostics {
|
match diagnostics {
|
||||||
Diagnostics::TextDocument(diagnostics) => {
|
Diagnostics::TextDocument(diagnostics) => {
|
||||||
publish_diagnostics_notification(url, diagnostics)?;
|
publish_diagnostics_notification(url, diagnostics);
|
||||||
}
|
}
|
||||||
Diagnostics::NotebookDocument(cell_diagnostics) => {
|
Diagnostics::NotebookDocument(cell_diagnostics) => {
|
||||||
for (cell_url, diagnostics) in cell_diagnostics {
|
for (cell_url, diagnostics) in cell_diagnostics {
|
||||||
publish_diagnostics_notification(cell_url, diagnostics)?;
|
publish_diagnostics_notification(cell_url, diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ impl SyncNotificationHandler for CancelNotificationHandler {
|
||||||
lsp_types::NumberOrString::String(id) => id.into(),
|
lsp_types::NumberOrString::String(id) => id.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = client.cancel(session, id);
|
client.cancel(session, id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
||||||
|
|
||||||
match key.path() {
|
match key.path() {
|
||||||
AnySystemPath::System(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,
|
Some(db) => db,
|
||||||
None => session.default_project_db_mut(),
|
None => session.default_project_db_mut(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::server::Result;
|
use crate::server::Result;
|
||||||
use crate::server::api::LSPResult;
|
|
||||||
use crate::server::api::diagnostics::publish_diagnostics;
|
use crate::server::api::diagnostics::publish_diagnostics;
|
||||||
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||||
use crate::session::Session;
|
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!(
|
tracing::trace!(
|
||||||
"Ignoring change event for `{system_path}` because it's not in any workspace"
|
"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 project_changed {
|
||||||
if client_capabilities.diagnostics_refresh {
|
if client_capabilities.diagnostics_refresh {
|
||||||
client
|
client.send_request::<types::request::WorkspaceDiagnosticRefresh>(
|
||||||
.send_request::<types::request::WorkspaceDiagnosticRefresh>(
|
|
||||||
session,
|
session,
|
||||||
(),
|
(),
|
||||||
|_, ()| {},
|
|_, ()| {},
|
||||||
)
|
);
|
||||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
|
||||||
} else {
|
} else {
|
||||||
for key in session.text_document_keys() {
|
for key in session.text_document_keys() {
|
||||||
publish_diagnostics(session, &key, client)?;
|
publish_diagnostics(session, &key, client)?;
|
||||||
|
@ -120,9 +117,7 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
|
||||||
}
|
}
|
||||||
|
|
||||||
if client_capabilities.inlay_refresh {
|
if client_capabilities.inlay_refresh {
|
||||||
client
|
client.send_request::<types::request::InlayHintRefreshRequest>(session, (), |_, ()| {});
|
||||||
.send_request::<types::request::InlayHintRefreshRequest>(session, (), |_, ()| {})
|
|
||||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -41,6 +41,8 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear_diagnostics(&key, client)
|
clear_diagnostics(&key, client);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
|
||||||
|
|
||||||
match key.path() {
|
match key.path() {
|
||||||
AnySystemPath::System(system_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,
|
Some(db) => db,
|
||||||
None => session.default_project_db_mut(),
|
None => session.default_project_db_mut(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,7 +40,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
||||||
|
|
||||||
match &path {
|
match &path {
|
||||||
AnySystemPath::System(system_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,
|
Some(db) => db,
|
||||||
None => session.default_project_db_mut(),
|
None => session.default_project_db_mut(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
use crate::server::schedule::Scheduler;
|
use crate::server::schedule::Scheduler;
|
||||||
use crate::server::{Server, api};
|
use crate::server::{Server, api};
|
||||||
|
use crate::session::ClientOptions;
|
||||||
use crate::session::client::Client;
|
use crate::session::client::Client;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use crossbeam::select;
|
use crossbeam::select;
|
||||||
use lsp_server::Message;
|
use lsp_server::Message;
|
||||||
use lsp_types::notification::Notification;
|
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 MainLoopSender = crossbeam::channel::Sender<Event>;
|
||||||
pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver<Event>;
|
pub(crate) type MainLoopReceiver = crossbeam::channel::Receiver<Event>;
|
||||||
|
@ -26,6 +30,10 @@ impl Server {
|
||||||
|
|
||||||
match next_event {
|
match next_event {
|
||||||
Event::Message(msg) => {
|
Event::Message(msg) => {
|
||||||
|
let Some(msg) = self.session.should_defer_message(msg) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let client = Client::new(
|
let client = Client::new(
|
||||||
self.main_loop_sender.clone(),
|
self.main_loop_sender.clone(),
|
||||||
self.connection.sender.clone(),
|
self.connection.sender.clone(),
|
||||||
|
@ -49,7 +57,7 @@ impl Server {
|
||||||
message: "Shutdown already requested".to_owned(),
|
message: "Shutdown already requested".to_owned(),
|
||||||
data: None,
|
data: None,
|
||||||
},
|
},
|
||||||
)?;
|
);
|
||||||
continue;
|
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.
|
/// Waits for the next message from the client or action.
|
||||||
///
|
///
|
||||||
/// Returns `Ok(None)` if the client connection is closed.
|
/// 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!(
|
select!(
|
||||||
recv(self.connection.receiver) -> msg => {
|
recv(self.connection.receiver) -> msg => {
|
||||||
// Ignore disconnect errors, they're handled by the main loop (it will exit).
|
// 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) {
|
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
|
let fs_watcher = self
|
||||||
.client_capabilities
|
.client_capabilities
|
||||||
.workspace
|
.workspace
|
||||||
|
@ -206,17 +276,13 @@ impl Server {
|
||||||
tracing::info!("File watcher successfully registered");
|
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,
|
&self.session,
|
||||||
lsp_types::RegistrationParams {
|
lsp_types::RegistrationParams {
|
||||||
registrations: vec![registration],
|
registrations: vec![registration],
|
||||||
},
|
},
|
||||||
response_handler,
|
response_handler,
|
||||||
) {
|
|
||||||
tracing::error!(
|
|
||||||
"An error occurred when trying to register the configuration file watcher: {err}"
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("The client does not support file system watching.");
|
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.
|
/// Retry a request that previously failed due to a salsa cancellation.
|
||||||
RetryRequest(lsp_server::Request),
|
RetryRequest(lsp_server::Request),
|
||||||
|
|
||||||
|
/// Initialize the workspace after the server received
|
||||||
|
/// the options from the client.
|
||||||
|
InitializeWorkspaces(Vec<(Url, ClientOptions)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -83,9 +83,7 @@ impl Task {
|
||||||
R: Serialize + Send + 'static,
|
R: Serialize + Send + 'static,
|
||||||
{
|
{
|
||||||
Self::sync(move |_, client| {
|
Self::sync(move |_, client| {
|
||||||
if let Err(err) = client.respond(&id, result) {
|
client.respond(&id, result);
|
||||||
tracing::error!("Unable to send immediate response: {err}");
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
//! Data model, state management, and configuration resolution.
|
//! Data model, state management, and configuration resolution.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::{Context, anyhow};
|
||||||
|
use lsp_server::Message;
|
||||||
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url};
|
use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url};
|
||||||
use options::GlobalOptions;
|
use options::GlobalOptions;
|
||||||
use ruff_db::Db;
|
use ruff_db::Db;
|
||||||
use ruff_db::files::{File, system_path_to_file};
|
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};
|
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||||
|
|
||||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||||
pub use self::index::DocumentQuery;
|
pub use self::index::DocumentQuery;
|
||||||
pub(crate) use self::options::{AllOptions, ClientOptions};
|
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::document::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||||
use crate::session::request_queue::RequestQueue;
|
use crate::session::request_queue::RequestQueue;
|
||||||
use crate::system::{AnySystemPath, LSPSystem};
|
use crate::system::{AnySystemPath, LSPSystem};
|
||||||
|
@ -40,8 +42,13 @@ pub struct Session {
|
||||||
/// [`index_mut`]: Session::index_mut
|
/// [`index_mut`]: Session::index_mut
|
||||||
index: Option<Arc<index::Index>>,
|
index: Option<Arc<index::Index>>,
|
||||||
|
|
||||||
/// Maps workspace folders to their respective project databases.
|
/// Maps workspace folders to their respective workspace.
|
||||||
projects_by_workspace_folder: BTreeMap<PathBuf, ProjectDatabase>,
|
workspaces: Workspaces,
|
||||||
|
|
||||||
|
/// The projects across all workspaces.
|
||||||
|
projects: BTreeMap<SystemPathBuf, ProjectDatabase>,
|
||||||
|
|
||||||
|
default_project: ProjectDatabase,
|
||||||
|
|
||||||
/// The global position encoding, negotiated during LSP initialization.
|
/// The global position encoding, negotiated during LSP initialization.
|
||||||
position_encoding: PositionEncoding,
|
position_encoding: PositionEncoding,
|
||||||
|
@ -54,6 +61,8 @@ pub struct Session {
|
||||||
|
|
||||||
/// Has the client requested the server to shutdown.
|
/// Has the client requested the server to shutdown.
|
||||||
shutdown_requested: bool,
|
shutdown_requested: bool,
|
||||||
|
|
||||||
|
deferred_messages: VecDeque<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
|
@ -61,32 +70,33 @@ impl Session {
|
||||||
client_capabilities: &ClientCapabilities,
|
client_capabilities: &ClientCapabilities,
|
||||||
position_encoding: PositionEncoding,
|
position_encoding: PositionEncoding,
|
||||||
global_options: GlobalOptions,
|
global_options: GlobalOptions,
|
||||||
workspace_folders: &[(Url, ClientOptions)],
|
workspace_folders: Vec<(Url, ClientOptions)>,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
let mut workspaces = BTreeMap::new();
|
|
||||||
let index = Arc::new(index::Index::new(global_options.into_settings()));
|
let index = Arc::new(index::Index::new(global_options.into_settings()));
|
||||||
|
|
||||||
// TODO: Consider workspace settings
|
let mut workspaces = Workspaces::default();
|
||||||
for (url, _) in workspace_folders {
|
for (url, options) in workspace_folders {
|
||||||
let path = url
|
workspaces.register(url, options)?;
|
||||||
.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 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 {
|
Ok(Self {
|
||||||
position_encoding,
|
position_encoding,
|
||||||
projects_by_workspace_folder: workspaces,
|
workspaces,
|
||||||
|
deferred_messages: VecDeque::new(),
|
||||||
index: Some(index),
|
index: Some(index),
|
||||||
|
default_project,
|
||||||
|
projects: BTreeMap::new(),
|
||||||
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||||
client_capabilities,
|
client_capabilities,
|
||||||
)),
|
)),
|
||||||
|
@ -111,6 +121,52 @@ impl Session {
|
||||||
self.shutdown_requested = requested;
|
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`
|
// 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.
|
// and `default_workspace_db_mut` but the borrow checker doesn't allow that.
|
||||||
// https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437
|
// 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.
|
/// or the default project if no project is found for the path.
|
||||||
pub(crate) fn project_db_or_default(&self, path: &AnySystemPath) -> &ProjectDatabase {
|
pub(crate) fn project_db_or_default(&self, path: &AnySystemPath) -> &ProjectDatabase {
|
||||||
path.as_system()
|
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())
|
.unwrap_or_else(|| self.default_project_db())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if
|
/// Returns a reference to the project's [`ProjectDatabase`] corresponding to the given path, if
|
||||||
/// any.
|
/// any.
|
||||||
pub(crate) fn project_db_for_path(&self, path: impl AsRef<Path>) -> Option<&ProjectDatabase> {
|
pub(crate) fn project_db_for_path(
|
||||||
self.projects_by_workspace_folder
|
&self,
|
||||||
|
path: impl AsRef<SystemPath>,
|
||||||
|
) -> Option<&ProjectDatabase> {
|
||||||
|
self.projects
|
||||||
.range(..=path.as_ref().to_path_buf())
|
.range(..=path.as_ref().to_path_buf())
|
||||||
.next_back()
|
.next_back()
|
||||||
.map(|(_, db)| db)
|
.map(|(_, db)| db)
|
||||||
|
@ -136,9 +195,9 @@ impl Session {
|
||||||
/// path, if any.
|
/// path, if any.
|
||||||
pub(crate) fn project_db_for_path_mut(
|
pub(crate) fn project_db_for_path_mut(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<SystemPath>,
|
||||||
) -> Option<&mut ProjectDatabase> {
|
) -> Option<&mut ProjectDatabase> {
|
||||||
self.projects_by_workspace_folder
|
self.projects
|
||||||
.range_mut(..=path.as_ref().to_path_buf())
|
.range_mut(..=path.as_ref().to_path_buf())
|
||||||
.next_back()
|
.next_back()
|
||||||
.map(|(_, db)| db)
|
.map(|(_, db)| db)
|
||||||
|
@ -147,23 +206,85 @@ impl Session {
|
||||||
/// Returns a reference to the default project [`ProjectDatabase`]. The default project is the
|
/// Returns a reference to the default project [`ProjectDatabase`]. The default project is the
|
||||||
/// minimum root path in the project map.
|
/// minimum root path in the project map.
|
||||||
pub(crate) fn default_project_db(&self) -> &ProjectDatabase {
|
pub(crate) fn default_project_db(&self) -> &ProjectDatabase {
|
||||||
// SAFETY: Currently, ty only support a single project.
|
&self.default_project
|
||||||
self.projects_by_workspace_folder.values().next().unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a mutable reference to the default project [`ProjectDatabase`].
|
/// Returns a mutable reference to the default project [`ProjectDatabase`].
|
||||||
pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase {
|
pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase {
|
||||||
// SAFETY: Currently, ty only support a single project.
|
&mut self.default_project
|
||||||
self.projects_by_workspace_folder
|
}
|
||||||
|
|
||||||
|
fn projects_mut(&mut self) -> impl Iterator<Item = &'_ mut ProjectDatabase> + '_ {
|
||||||
|
self.projects
|
||||||
.values_mut()
|
.values_mut()
|
||||||
.next()
|
.chain(std::iter::once(&mut self.default_project))
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn key_from_url(&self, url: Url) -> crate::Result<DocumentKey> {
|
pub(crate) fn key_from_url(&self, url: Url) -> crate::Result<DocumentKey> {
|
||||||
self.index().key_from_url(url)
|
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.
|
/// 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.
|
/// 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 {
|
fn index_mut(&mut self) -> MutIndexGuard {
|
||||||
let index = self.index.take().unwrap();
|
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
|
// Remove the `index` from each database. This drops the count of `Arc<Index>` down to 1
|
||||||
db.system_mut()
|
db.system_mut()
|
||||||
.as_any_mut()
|
.as_any_mut()
|
||||||
|
@ -250,11 +371,11 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
// There should now be exactly one reference to index which is self.index.
|
// 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 {
|
MutIndexGuard {
|
||||||
session: self,
|
session: self,
|
||||||
index,
|
index: Some(index),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +410,7 @@ impl Drop for MutIndexGuard<'_> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(index) = self.index.take() {
|
if let Some(index) = self.index.take() {
|
||||||
let index = Arc::new(index);
|
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()
|
db.system_mut()
|
||||||
.as_any_mut()
|
.as_any_mut()
|
||||||
.downcast_mut::<LSPSystem>()
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::Session;
|
use crate::Session;
|
||||||
use crate::server::{Action, ConnectionSender};
|
use crate::server::{Action, ConnectionSender};
|
||||||
use crate::server::{Event, MainLoopSender};
|
use crate::server::{Event, MainLoopSender};
|
||||||
use anyhow::{Context, anyhow};
|
|
||||||
use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError};
|
use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
@ -45,8 +44,7 @@ impl Client {
|
||||||
session: &Session,
|
session: &Session,
|
||||||
params: R::Params,
|
params: R::Params,
|
||||||
response_handler: impl FnOnce(&Client, R::Result) + Send + 'static,
|
response_handler: impl FnOnce(&Client, R::Result) + Send + 'static,
|
||||||
) -> crate::Result<()>
|
) where
|
||||||
where
|
|
||||||
R: lsp_types::request::Request,
|
R: lsp_types::request::Request,
|
||||||
{
|
{
|
||||||
let response_handler = Box::new(move |client: &Client, response: lsp_server::Response| {
|
let response_handler = Box::new(move |client: &Client, response: lsp_server::Response| {
|
||||||
|
@ -95,60 +93,64 @@ impl Client {
|
||||||
.outgoing()
|
.outgoing()
|
||||||
.register(response_handler);
|
.register(response_handler);
|
||||||
|
|
||||||
self.client_sender
|
if let Err(err) = self
|
||||||
|
.client_sender
|
||||||
.send(Message::Request(lsp_server::Request {
|
.send(Message::Request(lsp_server::Request {
|
||||||
id,
|
id,
|
||||||
method: R::METHOD.to_string(),
|
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)
|
tracing::error!(
|
||||||
})?;
|
"Failed to send request `{}` because the client sender is closed: {err}",
|
||||||
|
R::METHOD
|
||||||
Ok(())
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a notification to the client.
|
/// 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
|
where
|
||||||
N: lsp_types::notification::Notification,
|
N: lsp_types::notification::Notification,
|
||||||
{
|
{
|
||||||
let method = N::METHOD.to_string();
|
let method = N::METHOD.to_string();
|
||||||
|
|
||||||
|
if let Err(err) =
|
||||||
self.client_sender
|
self.client_sender
|
||||||
.send(lsp_server::Message::Notification(Notification::new(
|
.send(lsp_server::Message::Notification(Notification::new(
|
||||||
method, params,
|
method, params,
|
||||||
)))
|
)))
|
||||||
.map_err(|error| {
|
{
|
||||||
anyhow!(
|
tracing::error!(
|
||||||
"Failed to send notification (method={method}): {error}",
|
"Failed to send notification `{}` because the client sender is closed: {err}",
|
||||||
method = N::METHOD
|
N::METHOD
|
||||||
)
|
);
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a notification without any parameters to the client.
|
/// Sends a notification without any parameters to the client.
|
||||||
///
|
///
|
||||||
/// This is useful for notifications that don't require any data.
|
/// This is useful for notifications that don't require any data.
|
||||||
#[expect(dead_code)]
|
#[expect(dead_code)]
|
||||||
pub(crate) fn send_notification_no_params(&self, method: &str) -> crate::Result<()> {
|
pub(crate) fn send_notification_no_params(&self, method: &str) {
|
||||||
|
if let Err(err) =
|
||||||
self.client_sender
|
self.client_sender
|
||||||
.send(lsp_server::Message::Notification(Notification::new(
|
.send(lsp_server::Message::Notification(Notification::new(
|
||||||
method.to_string(),
|
method.to_string(),
|
||||||
Value::Null,
|
Value::Null,
|
||||||
)))
|
)))
|
||||||
.map_err(|error| anyhow!("Failed to send notification (method={method}): {error}",))
|
{
|
||||||
|
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.
|
/// 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
|
/// 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).
|
/// and checked for cancellation (each request must have exactly one response).
|
||||||
pub(crate) fn respond<R>(
|
pub(crate) fn respond<R>(&self, id: &RequestId, result: crate::server::Result<R>)
|
||||||
&self,
|
|
||||||
id: &RequestId,
|
|
||||||
result: crate::server::Result<R>,
|
|
||||||
) -> crate::Result<()>
|
|
||||||
where
|
where
|
||||||
R: serde::Serialize,
|
R: serde::Serialize,
|
||||||
{
|
{
|
||||||
|
@ -161,17 +163,13 @@ impl Client {
|
||||||
|
|
||||||
self.main_loop_sender
|
self.main_loop_sender
|
||||||
.send(Event::Action(Action::SendResponse(response)))
|
.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.
|
/// 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.
|
/// The response isn't sent immediately. Instead, it's queued up in the main loop.
|
||||||
pub(crate) fn respond_err(
|
pub(crate) fn respond_err(&self, id: RequestId, error: lsp_server::ResponseError) {
|
||||||
&self,
|
|
||||||
id: RequestId,
|
|
||||||
error: lsp_server::ResponseError,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
let response = lsp_server::Response {
|
let response = lsp_server::Response {
|
||||||
id,
|
id,
|
||||||
result: None,
|
result: None,
|
||||||
|
@ -180,23 +178,19 @@ impl Client {
|
||||||
|
|
||||||
self.main_loop_sender
|
self.main_loop_sender
|
||||||
.send(Event::Action(Action::SendResponse(response)))
|
.send(Event::Action(Action::SendResponse(response)))
|
||||||
.map_err(|error| anyhow!("Failed to send response: {error}"))
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a message to the user.
|
/// Shows a message to the user.
|
||||||
///
|
///
|
||||||
/// This opens a pop up in VS Code showing `message`.
|
/// This opens a pop up in VS Code showing `message`.
|
||||||
pub(crate) fn show_message(
|
pub(crate) fn show_message(&self, message: impl Display, message_type: lsp_types::MessageType) {
|
||||||
&self,
|
|
||||||
message: impl Display,
|
|
||||||
message_type: lsp_types::MessageType,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
self.send_notification::<lsp_types::notification::ShowMessage>(
|
self.send_notification::<lsp_types::notification::ShowMessage>(
|
||||||
lsp_types::ShowMessageParams {
|
lsp_types::ShowMessageParams {
|
||||||
typ: message_type,
|
typ: message_type,
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a request to display a warning to the client with a formatted message. The warning is
|
/// 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.
|
/// Logs an error if the message could not be sent.
|
||||||
pub(crate) fn show_warning_message(&self, message: impl Display) {
|
pub(crate) fn show_warning_message(&self, message: impl Display) {
|
||||||
let result = self.show_message(message, lsp_types::MessageType::WARNING);
|
self.show_message(message, lsp_types::MessageType::WARNING);
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
tracing::error!("Failed to send warning message to the client: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a request to display an error to the client with a formatted message. The error is
|
/// 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.
|
/// Logs an error if the message could not be sent.
|
||||||
pub(crate) fn show_error_message(&self, message: impl Display) {
|
pub(crate) fn show_error_message(&self, message: impl Display) {
|
||||||
let result = self.show_message(message, lsp_types::MessageType::ERROR);
|
self.show_message(message, lsp_types::MessageType::ERROR);
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
tracing::error!("Failed to send error message to the client: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-queues this request after a salsa cancellation for a retry.
|
/// 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.
|
/// 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
|
self.main_loop_sender
|
||||||
.send(Event::Action(Action::RetryRequest(request)))
|
.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);
|
let method_name = session.request_queue_mut().incoming_mut().cancel(&id);
|
||||||
|
|
||||||
if let Some(method_name) = method_name {
|
if let Some(method_name) = method_name {
|
||||||
|
@ -245,14 +235,18 @@ impl Client {
|
||||||
|
|
||||||
// Use `client_sender` here instead of `respond_err` because
|
// Use `client_sender` here instead of `respond_err` because
|
||||||
// `respond_err` filters out responses for canceled requests (which we just did!).
|
// `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 {
|
.send(Message::Response(lsp_server::Response {
|
||||||
id,
|
id,
|
||||||
result: None,
|
result: None,
|
||||||
error: Some(error),
|
error: Some(error),
|
||||||
}))?;
|
}))
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to send cancellation response for request `{method_name}` because the client sender is closed: {err}",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,14 +24,7 @@ pub(crate) struct GlobalOptions {
|
||||||
|
|
||||||
impl GlobalOptions {
|
impl GlobalOptions {
|
||||||
pub(crate) fn into_settings(self) -> ClientSettings {
|
pub(crate) fn into_settings(self) -> ClientSettings {
|
||||||
ClientSettings {
|
self.client.into_settings()
|
||||||
disable_language_services: self
|
|
||||||
.client
|
|
||||||
.python
|
|
||||||
.and_then(|python| python.ty)
|
|
||||||
.and_then(|ty| ty.disable_language_services)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +49,19 @@ pub(crate) struct ClientOptions {
|
||||||
python: Option<Python>,
|
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
|
// 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
|
// 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.*".
|
// all settings and not just the ones in "python.*".
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue