diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 6256e5eb76..93cc04ac7f 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -18,13 +18,16 @@ use std::panic::{PanicHookInfo, RefUnwindSafe}; use std::sync::Arc; mod api; +mod lazy_work_done_progress; mod main_loop; mod schedule; use crate::session::client::Client; pub(crate) use api::Error; pub(crate) use api::publish_settings_diagnostics; -pub(crate) use main_loop::{Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender}; +pub(crate) use main_loop::{ + Action, ConnectionSender, Event, MainLoopReceiver, MainLoopSender, SendRequest, +}; pub(crate) type Result = std::result::Result; pub struct Server { @@ -198,7 +201,9 @@ impl Server { inter_file_dependencies: true, // TODO: Dynamically register for workspace diagnostics. workspace_diagnostics: diagnostic_mode.is_workspace(), - ..Default::default() + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: Some(diagnostic_mode.is_workspace()), + }, })), text_document_sync: Some(TextDocumentSyncCapability::Options( TextDocumentSyncOptions { diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index c3a2b77bd3..a31606d9d2 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -1,13 +1,3 @@ -use std::collections::BTreeMap; - -use crate::server::Result; -use crate::server::api::diagnostics::{Diagnostics, to_lsp_diagnostic}; -use crate::server::api::traits::{ - BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, -}; -use crate::session::SessionSnapshot; -use crate::session::client::Client; -use crate::system::file_to_url; use lsp_types::request::WorkspaceDiagnosticRequest; use lsp_types::{ FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport, Url, @@ -15,6 +5,20 @@ use lsp_types::{ WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport, WorkspaceUnchangedDocumentDiagnosticReport, }; +use ruff_db::files::File; +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use ty_project::ProgressReporter; + +use crate::server::Result; +use crate::server::api::diagnostics::{Diagnostics, to_lsp_diagnostic}; +use crate::server::api::traits::{ + BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::server::lazy_work_done_progress::LazyWorkDoneProgress; +use crate::session::SessionSnapshot; +use crate::session::client::Client; +use crate::system::file_to_url; pub(crate) struct WorkspaceDiagnosticRequestHandler; @@ -25,7 +29,7 @@ impl RequestHandler for WorkspaceDiagnosticRequestHandler { impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { fn run( snapshot: SessionSnapshot, - _client: &Client, + client: &Client, params: WorkspaceDiagnosticParams, ) -> Result { let index = snapshot.index(); @@ -44,10 +48,24 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { .map(|prev| (prev.uri, prev.value)) .collect(); + // Use the work done progress token from the client request, if provided + // Note: neither VS Code nor Zed currently support this, + // see https://github.com/microsoft/vscode-languageserver-node/issues/528 + // That's why we fall back to server-initiated progress if no token is provided. + let work_done_progress = LazyWorkDoneProgress::new( + client, + params.work_done_progress_params.work_done_token, + "Checking", + snapshot.resolved_client_capabilities(), + ); + + // Collect all diagnostics from all projects with their database references let mut items = Vec::new(); for db in snapshot.projects() { - let diagnostics = db.check(); + let diagnostics = db.check_with_reporter( + &mut WorkspaceDiagnosticsProgressReporter::new(work_done_progress.clone()), + ); // Group diagnostics by URL let mut diagnostics_by_url: BTreeMap> = BTreeMap::default(); @@ -152,3 +170,55 @@ impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler { } } } + +struct WorkspaceDiagnosticsProgressReporter { + total_files: usize, + checked_files: AtomicUsize, + work_done: LazyWorkDoneProgress, +} + +impl WorkspaceDiagnosticsProgressReporter { + fn new(work_done: LazyWorkDoneProgress) -> Self { + Self { + total_files: 0, + checked_files: AtomicUsize::new(0), + work_done, + } + } + + fn report_progress(&self) { + let checked = self.checked_files.load(Ordering::Relaxed); + let total = self.total_files; + + #[allow(clippy::cast_possible_truncation)] + let percentage = if total > 0 { + Some((checked * 100 / total) as u32) + } else { + None + }; + + self.work_done + .report_progress(format!("{checked}/{total} files"), percentage); + + if checked == total { + self.work_done + .set_finish_message(format!("Checked {total} files")); + } + } +} + +impl ProgressReporter for WorkspaceDiagnosticsProgressReporter { + fn set_files(&mut self, files: usize) { + self.total_files += files; + self.report_progress(); + } + + fn report_file(&self, _file: &File) { + let checked = self.checked_files.fetch_add(1, Ordering::Relaxed) + 1; + + if checked % 10 == 0 || checked == self.total_files { + // Report progress every 10 files or when all files are checked + self.report_progress(); + } + } +} diff --git a/crates/ty_server/src/server/lazy_work_done_progress.rs b/crates/ty_server/src/server/lazy_work_done_progress.rs new file mode 100644 index 0000000000..8acab9c901 --- /dev/null +++ b/crates/ty_server/src/server/lazy_work_done_progress.rs @@ -0,0 +1,167 @@ +use crate::session::ResolvedClientCapabilities; +use crate::session::client::Client; +use lsp_types::request::WorkDoneProgressCreate; +use lsp_types::{ + ProgressParams, ProgressParamsValue, ProgressToken, WorkDoneProgress, WorkDoneProgressBegin, + WorkDoneProgressCreateParams, WorkDoneProgressEnd, WorkDoneProgressReport, +}; +use std::fmt::Display; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static SERVER_WORK_DONE_TOKENS: AtomicUsize = AtomicUsize::new(0); + +/// A [work done progress][work-done-progress] that uses the client provided token if available, +/// but falls back to a server initiated progress if supported by the client. +/// +/// The LSP specification supports client and server initiated work done progress reporting: +/// * Client: Many requests have a work done progress token or extend `WorkDoneProgressParams`. +/// For those requests, a server can ask clients to start a work done progress report by +/// setting the work done capability for that request in the server's capabilities during initialize. +/// However, as of today (July 2025), VS code and Zed don't support client initiated work done progress +/// tokens except for the `initialize` request (). +/// * Server: A server can initiate a work done progress report by sending a `WorkDoneProgressCreate` request +/// with a token, which the client can then use to report progress (except during `initialize`). +/// +/// This work done progress supports both clients that provide a work done progress token in their requests +/// and clients that do not. If the client does not provide a token, the server will +/// initiate a work done progress report using a unique string token. +/// +/// ## Server Initiated Progress +/// +/// The implementation initiates a work done progress report lazily when no token is provided in the request. +/// This creation happens async and the LSP specification requires that a server only +/// sends `$/progress` notifications with that token if the create request was successful (no error): +/// +/// > code and message set in case an exception happens during the 'window/workDoneProgress/create' request. +/// > In case an error occurs a server must not send any progress notification +/// > using the token provided in the WorkDoneProgressCreateParams. +/// +/// The implementation doesn't block on the server response because it feels unfortunate to delay +/// a client request only so that ty can show a progress bar. Therefore, the progress reporting +/// will not be available immediately. +/// +/// [work-done-progress]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workDoneProgress +#[derive(Clone)] +pub(super) struct LazyWorkDoneProgress { + inner: Arc, +} + +impl LazyWorkDoneProgress { + pub(super) fn new( + client: &Client, + request_token: Option, + title: &str, + capabilities: ResolvedClientCapabilities, + ) -> Self { + if let Some(token) = &request_token { + Self::send_begin(client, token.clone(), title.to_string()); + } + + let is_server_initiated = request_token.is_none(); + + let once_token = std::sync::OnceLock::new(); + if let Some(token) = request_token { + // SAFETY: The token is guaranteed to be not set yet because we only created it above. + once_token.set(token).unwrap(); + } + + let work_done = Self { + inner: Arc::new(Inner { + token: once_token, + finish_message: std::sync::Mutex::default(), + client: client.clone(), + }), + }; + + if is_server_initiated && capabilities.supports_work_done_progress() { + // Use a string token because Zed does not support numeric tokens + let token = ProgressToken::String(format!( + "ty-{}", + SERVER_WORK_DONE_TOKENS.fetch_add(1, Ordering::Relaxed) + )); + let work_done = work_done.clone(); + let title = title.to_string(); + + client.send_deferred_request::( + WorkDoneProgressCreateParams { + token: token.clone(), + }, + move |client, ()| { + Self::send_begin(client, token.clone(), title); + // SAFETY: We only take this branch if `request_token` was `None` + // and we only issue a single request (without retry). + work_done.inner.token.set(token).unwrap(); + }, + ); + } + + work_done + } + + pub(super) fn set_finish_message(&self, message: String) { + let mut finish_message = self.inner.finish_message.lock().unwrap(); + + *finish_message = Some(message); + } + + fn send_begin(client: &Client, token: ProgressToken, title: String) { + client.send_notification::(ProgressParams { + token, + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin { + title, + cancellable: Some(false), + message: None, + percentage: Some(0), + })), + }); + } + + /// Sends a progress report with the given message and optional percentage. + pub(super) fn report_progress(&self, message: impl Display, percentage: Option) { + let Some(token) = self.inner.token.get() else { + return; + }; + + self.inner + .client + .send_notification::(ProgressParams { + token: token.clone(), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report( + WorkDoneProgressReport { + cancellable: Some(false), + message: Some(message.to_string()), + percentage, + }, + )), + }); + } +} + +struct Inner { + token: std::sync::OnceLock, + finish_message: std::sync::Mutex>, + client: Client, +} + +impl Drop for Inner { + fn drop(&mut self) { + let Some(token) = self.token.get() else { + return; + }; + + let finish_message = self + .finish_message + .lock() + .ok() + .and_then(|mut message| message.take()); + + self.client + .send_notification::(ProgressParams { + token: token.clone(), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { + message: finish_message, + })), + }); + } +} diff --git a/crates/ty_server/src/server/main_loop.rs b/crates/ty_server/src/server/main_loop.rs index d34cbc404c..e19ad2ba53 100644 --- a/crates/ty_server/src/server/main_loop.rs +++ b/crates/ty_server/src/server/main_loop.rs @@ -1,7 +1,7 @@ use crate::server::schedule::Scheduler; use crate::server::{Server, api}; use crate::session::ClientOptions; -use crate::session::client::Client; +use crate::session::client::{Client, ClientResponseHandler}; use anyhow::anyhow; use crossbeam::select; use lsp_server::Message; @@ -87,7 +87,7 @@ impl Server { .outgoing_mut() .complete(&response.id) { - handler(&client, response); + handler.handle_response(&client, response); } else { tracing::error!( "Received a response with ID {}, which was not expected", @@ -139,6 +139,9 @@ impl Server { ); } } + + Action::SendRequest(request) => client.send_request_raw(&self.session, request), + Action::InitializeWorkspaces(workspaces_with_options) => { self.session .initialize_workspaces(workspaces_with_options, &client); @@ -300,6 +303,9 @@ pub(crate) enum Action { /// Retry a request that previously failed due to a salsa cancellation. RetryRequest(lsp_server::Request), + /// Send a request from the server to the client. + SendRequest(SendRequest), + /// Initialize the workspace after the server received /// the options from the client. InitializeWorkspaces(Vec<(Url, ClientOptions)>), @@ -312,3 +318,18 @@ pub(crate) enum Event { Action(Action), } + +pub(crate) struct SendRequest { + pub(crate) method: String, + pub(crate) params: serde_json::Value, + pub(crate) response_handler: ClientResponseHandler, +} + +impl std::fmt::Debug for SendRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SendRequest") + .field("method", &self.method) + .field("params", &self.params) + .finish_non_exhaustive() + } +} diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 2fb386aa9f..c817402af9 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -1,10 +1,5 @@ //! Data model, state management, and configuration resolution. -use std::collections::{BTreeMap, VecDeque}; -use std::ops::{Deref, DerefMut}; -use std::panic::RefUnwindSafe; -use std::sync::Arc; - use anyhow::{Context, anyhow}; use index::DocumentQueryError; use lsp_server::Message; @@ -15,6 +10,10 @@ use options::GlobalOptions; use ruff_db::Db; use ruff_db::files::File; use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use std::collections::{BTreeMap, VecDeque}; +use std::ops::{Deref, DerefMut}; +use std::panic::RefUnwindSafe; +use std::sync::Arc; use ty_project::metadata::Options; use ty_project::watch::ChangeEvent; use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata}; @@ -339,7 +338,6 @@ impl Session { client: &Client, ) { assert!(!self.workspaces.all_initialized()); - for (url, options) in workspace_settings { tracing::debug!("Initializing workspace `{url}`"); @@ -453,6 +451,7 @@ impl Session { .collect(), index: self.index.clone().unwrap(), position_encoding: self.position_encoding, + resolved_client_capabilities: self.resolved_client_capabilities, } } @@ -643,6 +642,7 @@ pub(crate) struct SessionSnapshot { projects: Vec, index: Arc, position_encoding: PositionEncoding, + resolved_client_capabilities: ResolvedClientCapabilities, } impl SessionSnapshot { @@ -657,6 +657,10 @@ impl SessionSnapshot { pub(crate) fn position_encoding(&self) -> PositionEncoding { self.position_encoding } + + pub(crate) fn resolved_client_capabilities(&self) -> ResolvedClientCapabilities { + self.resolved_client_capabilities + } } #[derive(Debug, Default)] diff --git a/crates/ty_server/src/session/capabilities.rs b/crates/ty_server/src/session/capabilities.rs index afa637ee1b..23083a56c6 100644 --- a/crates/ty_server/src/session/capabilities.rs +++ b/crates/ty_server/src/session/capabilities.rs @@ -17,6 +17,7 @@ bitflags::bitflags! { const SIGNATURE_LABEL_OFFSET_SUPPORT = 1 << 8; const SIGNATURE_ACTIVE_PARAMETER_SUPPORT = 1 << 9; const HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT = 1 << 10; + const WORK_DONE_PROGRESS = 1 << 11; } } @@ -76,6 +77,11 @@ impl ResolvedClientCapabilities { self.contains(Self::HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT) } + /// Returns `true` if the client supports work done progress. + pub(crate) const fn supports_work_done_progress(self) -> bool { + self.contains(Self::WORK_DONE_PROGRESS) + } + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { let mut flags = Self::empty(); @@ -191,6 +197,15 @@ impl ResolvedClientCapabilities { flags |= Self::HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT; } + if client_capabilities + .window + .as_ref() + .and_then(|window| window.work_done_progress) + .unwrap_or_default() + { + flags |= Self::WORK_DONE_PROGRESS; + } + flags } } diff --git a/crates/ty_server/src/session/client.rs b/crates/ty_server/src/session/client.rs index eb8aa406b2..674b127e16 100644 --- a/crates/ty_server/src/session/client.rs +++ b/crates/ty_server/src/session/client.rs @@ -1,14 +1,12 @@ use crate::Session; -use crate::server::{Action, ConnectionSender}; +use crate::server::{Action, ConnectionSender, SendRequest}; use crate::server::{Event, MainLoopSender}; use lsp_server::{ErrorCode, Message, Notification, RequestId, ResponseError}; use serde_json::Value; use std::any::TypeId; use std::fmt::Display; -pub(crate) type ClientResponseHandler = Box; - -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct Client { /// Channel to send messages back to the main loop. main_loop_sender: MainLoopSender, @@ -33,12 +31,9 @@ impl Client { /// The `response_handler` will be dispatched as soon as the client response /// is processed on the main-loop. The handler always runs on the main-loop thread. /// - /// # Note - /// This method takes a `session` so that we can register the pending-request - /// and send the response directly to the client. If this ever becomes too limiting (because we - /// need to send a request from somewhere where we don't have access to session), consider introducing - /// a new `send_deferred_request` method that doesn't take a session and instead sends - /// an `Action` to the main loop to send the request (the main loop has always access to session). + /// Use [`self.send_deferred_request`] if you are in a background task + /// where you don't have access to the session. But note, that the + /// request won't be send immediately, but rather queued up in the main loop pub(crate) fn send_request( &self, session: &Session, @@ -47,63 +42,56 @@ impl Client { ) where R: lsp_types::request::Request, { - let response_handler = Box::new(move |client: &Client, response: lsp_server::Response| { - let _span = - tracing::debug_span!("client_response", id=%response.id, method = R::METHOD) - .entered(); + self.send_request_raw( + session, + SendRequest { + method: R::METHOD.to_string(), + params: serde_json::to_value(params).expect("Params to be serializable"), + response_handler: ClientResponseHandler::for_request::(response_handler), + }, + ); + } - match (response.error, response.result) { - (Some(err), _) => { - tracing::error!( - "Got an error from the client (code {code}, method {method}): {message}", - code = err.code, - message = err.message, - method = R::METHOD - ); - } - (None, Some(response)) => match serde_json::from_value(response) { - Ok(response) => response_handler(client, response), - Err(error) => { - tracing::error!( - "Failed to deserialize client response (method={method}): {error}", - method = R::METHOD - ); - } - }, - (None, None) => { - if TypeId::of::() == TypeId::of::<()>() { - // We can't call `response_handler(())` directly here, but - // since we _know_ the type expected is `()`, we can use - // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, - // so this branch works in the general case but we'll only - // hit it if the concrete type is `()`, so the `unwrap()` is safe here. - response_handler(client, serde_json::from_value(Value::Null).unwrap()); - } else { - tracing::error!( - "Invalid client response: did not contain a result or error (method={method})", - method = R::METHOD - ); - } - } - } - }); + /// Sends a request of kind `R` to the client, with associated parameters. + /// + /// The request isn't sent immediately, but rather queued up in the main loop. + /// The `response_handler` will be dispatched as soon as the client response + /// is processed on the main-loop. The handler always runs on the main-loop thread. + /// + /// Use [`self.send_request`] if you are in a foreground task and have access to the session. + pub(crate) fn send_deferred_request( + &self, + params: R::Params, + response_handler: impl FnOnce(&Client, R::Result) + Send + 'static, + ) where + R: lsp_types::request::Request, + { + self.main_loop_sender + .send(Event::Action(Action::SendRequest(SendRequest { + method: R::METHOD.to_string(), + params: serde_json::to_value(params).expect("Params to be serializable"), + response_handler: ClientResponseHandler::for_request::(response_handler), + }))) + .unwrap(); + } + pub(crate) fn send_request_raw(&self, session: &Session, request: SendRequest) { let id = session .request_queue() .outgoing() - .register(response_handler); + .register(request.response_handler); 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).expect("Params to be serializable"), + method: request.method.clone(), + params: request.params, })) { tracing::error!( "Failed to send request `{}` because the client sender is closed: {err}", - R::METHOD + request.method ); } } @@ -250,3 +238,62 @@ impl Client { } } } + +/// Type erased handler for client responses. +#[allow(clippy::type_complexity)] +pub(crate) struct ClientResponseHandler(Box); + +impl ClientResponseHandler { + fn for_request(response_handler: impl FnOnce(&Client, R::Result) + Send + 'static) -> Self + where + R: lsp_types::request::Request, + { + Self(Box::new( + move |client: &Client, response: lsp_server::Response| { + let _span = + tracing::debug_span!("client_response", id=%response.id, method = R::METHOD) + .entered(); + + match (response.error, response.result) { + (Some(err), _) => { + tracing::error!( + "Got an error from the client (code {code}, method {method}): {message}", + code = err.code, + message = &err.message, + method = R::METHOD + ); + } + (None, Some(response)) => match serde_json::from_value(response) { + Ok(response) => response_handler(client, response), + Err(error) => { + tracing::error!( + "Failed to deserialize client response (method={method}): {error}", + method = R::METHOD + ); + } + }, + (None, None) => { + if TypeId::of::() == TypeId::of::<()>() { + // We can't call `response_handler(())` directly here, but + // since we _know_ the type expected is `()`, we can use + // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, + // so this branch works in the general case but we'll only + // hit it if the concrete type is `()`, so the `unwrap()` is safe here. + response_handler(client, serde_json::from_value(Value::Null).unwrap()); + } else { + tracing::error!( + "Invalid client response: did not contain a result or error (method={method})", + method = R::METHOD + ); + } + } + } + }, + )) + } + + pub(crate) fn handle_response(self, client: &Client, response: lsp_server::Response) { + let handler = self.0; + handler(client, response); + } +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 8080bfdcb0..c738ded5a5 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -658,7 +658,11 @@ impl TestServer { let params = WorkspaceDiagnosticParams { identifier: Some("ty".to_string()), previous_result_ids: previous_result_ids.unwrap_or_default(), - work_done_progress_params: WorkDoneProgressParams::default(), + work_done_progress_params: WorkDoneProgressParams { + work_done_token: Some(lsp_types::NumberOrString::String( + "test-progress-token".to_string(), + )), + }, partial_result_params: PartialResultParams::default(), }; diff --git a/crates/ty_server/tests/e2e/pull_diagnostics.rs b/crates/ty_server/tests/e2e/pull_diagnostics.rs index 73a1a727ae..d8c0516bb0 100644 --- a/crates/ty_server/tests/e2e/pull_diagnostics.rs +++ b/crates/ty_server/tests/e2e/pull_diagnostics.rs @@ -5,7 +5,7 @@ use lsp_types::{ use ruff_db::system::SystemPath; use ty_server::{ClientOptions, DiagnosticMode}; -use crate::TestServerBuilder; +use crate::{TestServer, TestServerBuilder}; #[test] fn on_did_open() -> Result<()> { @@ -234,8 +234,12 @@ def foo() -> str: // First request with no previous result IDs let first_response = server.workspace_diagnostic_request(None)?; + insta::assert_debug_snapshot!("workspace_diagnostic_initial_state", first_response); + // Consume all progress notifications sent during workspace diagnostics + consume_all_progress_notifications(&mut server)?; + // Extract result IDs from the first response let previous_result_ids = match first_response { WorkspaceDiagnosticReportResult::Report(report) => { @@ -317,6 +321,10 @@ def foo() -> str: // - File D: Full report (diagnostic content changed) // - File E: Full report (the range changes) let second_response = server.workspace_diagnostic_request(Some(previous_result_ids))?; + + // Consume all progress notifications sent during the second workspace diagnostics + consume_all_progress_notifications(&mut server)?; + insta::assert_debug_snapshot!("workspace_diagnostic_after_changes", second_response); Ok(()) @@ -328,3 +336,31 @@ fn filter_result_id() -> insta::internals::SettingsBindDropGuard { settings.add_filter(r#""[a-f0-9]{16}""#, r#""[RESULT_ID]""#); settings.bind_to_scope() } + +fn consume_all_progress_notifications(server: &mut TestServer) -> Result<()> { + // Always consume Begin + let begin_params = server.await_notification::()?; + + // The params are already the ProgressParams type + let lsp_types::ProgressParamsValue::WorkDone(lsp_types::WorkDoneProgress::Begin(_)) = + begin_params.value + else { + return Err(anyhow::anyhow!("Expected Begin progress notification")); + }; + + // Consume Report notifications - there may be multiple based on number of files + // Keep consuming until we hit the End notification + loop { + let params = server.await_notification::()?; + + if let lsp_types::ProgressParamsValue::WorkDone(lsp_types::WorkDoneProgress::End(_)) = + params.value + { + // Found the End notification, we're done + break; + } + // Otherwise it's a Report notification, continue + } + + Ok(()) +} diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index e47d6bd5b9..2fdd5481e6 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -64,7 +64,8 @@ expression: initialization_result "diagnosticProvider": { "identifier": "ty", "interFileDependencies": true, - "workspaceDiagnostics": false + "workspaceDiagnostics": false, + "workDoneProgress": false } }, "serverInfo": { diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index e47d6bd5b9..2fdd5481e6 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -64,7 +64,8 @@ expression: initialization_result "diagnosticProvider": { "identifier": "ty", "interFileDependencies": true, - "workspaceDiagnostics": false + "workspaceDiagnostics": false, + "workDoneProgress": false } }, "serverInfo": {