mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 12:05:57 +00:00 
			
		
		
		
	[ty] Implement long-polling for workspace diagnsotics (#19670)
	
		
			
	
		
	
	
		
	
		
			Some checks are pending
		
		
	
	
		
			
				
	
				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 / 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 / mkdocs (push) Waiting to run
				
			
		
			
				
	
				CI / cargo build (msrv) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / cargo fuzz build (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 / python package (push) Waiting to run
				
			
		
			
				
	
				CI / pre-commit (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 / 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 / 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 / mkdocs (push) Waiting to run
				
			CI / cargo build (msrv) (push) Blocked by required conditions
				
			CI / cargo fuzz build (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 / python package (push) Waiting to run
				
			CI / pre-commit (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
				
			This commit is contained in:
		
							parent
							
								
									736c4ab05a
								
							
						
					
					
						commit
						f473f6b6e5
					
				
					 31 changed files with 1138 additions and 201 deletions
				
			
		|  | @ -219,13 +219,11 @@ where | |||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let result = ruff_db::panic::catch_unwind(|| { | ||||
|             if let Err(error) = ruff_db::panic::catch_unwind(|| { | ||||
|                 let snapshot = snapshot; | ||||
|                 R::run(snapshot.0, client, params) | ||||
|             }); | ||||
| 
 | ||||
|             if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) { | ||||
|                 respond::<R>(&id, response, client); | ||||
|                 R::handle_request(&id, snapshot.0, client, params); | ||||
|             }) { | ||||
|                 panic_response::<R>(&id, client, &error, retry); | ||||
|             } | ||||
|         }) | ||||
|     })) | ||||
|  | @ -288,58 +286,50 @@ where | |||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let result = ruff_db::panic::catch_unwind(|| { | ||||
|                 R::run_with_snapshot(&db, snapshot, client, params) | ||||
|             }); | ||||
| 
 | ||||
|             if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) { | ||||
|                 respond::<R>(&id, response, client); | ||||
|             if let Err(error) = ruff_db::panic::catch_unwind(|| { | ||||
|                 R::handle_request(&id, &db, snapshot, client, params); | ||||
|             }) { | ||||
|                 panic_response::<R>(&id, client, &error, retry); | ||||
|             } | ||||
|         }) | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
| fn request_result_to_response<R>( | ||||
| fn panic_response<R>( | ||||
|     id: &RequestId, | ||||
|     client: &Client, | ||||
|     result: std::result::Result< | ||||
|         Result<<<R as RequestHandler>::RequestType as Request>::Result>, | ||||
|         PanicError, | ||||
|     >, | ||||
|     error: &PanicError, | ||||
|     request: Option<lsp_server::Request>, | ||||
| ) -> Option<Result<<<R as RequestHandler>::RequestType as Request>::Result>> | ||||
| where | ||||
| ) where | ||||
|     R: traits::RetriableRequestHandler, | ||||
| { | ||||
|     match result { | ||||
|         Ok(response) => Some(response), | ||||
|         Err(error) => { | ||||
|             // Check if the request was canceled due to some modifications to the salsa database.
 | ||||
|             if error.payload.downcast_ref::<salsa::Cancelled>().is_some() { | ||||
|                 // If the query supports retry, re-queue the request.
 | ||||
|                 // The query is still likely to succeed if the user modified any other document.
 | ||||
|                 if let Some(request) = request { | ||||
|                     tracing::trace!( | ||||
|                         "request id={} method={} was cancelled by salsa, re-queueing for retry", | ||||
|                         request.id, | ||||
|                         request.method | ||||
|                     ); | ||||
|                     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()); | ||||
|                 } | ||||
|                 None | ||||
|             } else { | ||||
|                 Some(Err(Error { | ||||
|                     code: lsp_server::ErrorCode::InternalError, | ||||
|                     error: anyhow!("request handler {error}"), | ||||
|                 })) | ||||
|             } | ||||
|     // Check if the request was canceled due to some modifications to the salsa database.
 | ||||
|     if error.payload.downcast_ref::<salsa::Cancelled>().is_some() { | ||||
|         // If the query supports retry, re-queue the request.
 | ||||
|         // The query is still likely to succeed if the user modified any other document.
 | ||||
|         if let Some(request) = request { | ||||
|             tracing::trace!( | ||||
|                 "request id={} method={} was cancelled by salsa, re-queueing for retry", | ||||
|                 request.id, | ||||
|                 request.method | ||||
|             ); | ||||
|             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()); | ||||
|         } | ||||
|     } else { | ||||
|         respond::<R>( | ||||
|             id, | ||||
|             Err(Error { | ||||
|                 code: lsp_server::ErrorCode::InternalError, | ||||
|                 error: anyhow!("request handler {error}"), | ||||
|             }), | ||||
|             client, | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -352,7 +342,13 @@ fn sync_notification_task<N: traits::SyncNotificationHandler>( | |||
|         if let Err(err) = N::run(session, client, params) { | ||||
|             tracing::error!("An error occurred while running {id}: {err}"); | ||||
|             client.show_error_message("ty encountered a problem. Check the logs for more details."); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // If there's a pending workspace diagnostic long-polling request,
 | ||||
|         // resume it, but only if the session revision changed (e.g. because some document changed).
 | ||||
|         session.resume_suspended_workspace_diagnostic_request(client); | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,17 +26,29 @@ pub(super) struct Diagnostics<'a> { | |||
| } | ||||
| 
 | ||||
| impl Diagnostics<'_> { | ||||
|     pub(super) fn result_id_from_hash(diagnostics: &[ruff_db::diagnostic::Diagnostic]) -> String { | ||||
|     /// Computes the result ID for `diagnostics`.
 | ||||
|     ///
 | ||||
|     /// Returns `None` if there are no diagnostics.
 | ||||
|     pub(super) fn result_id_from_hash( | ||||
|         diagnostics: &[ruff_db::diagnostic::Diagnostic], | ||||
|     ) -> Option<String> { | ||||
|         if diagnostics.is_empty() { | ||||
|             return None; | ||||
|         } | ||||
| 
 | ||||
|         // Generate result ID based on raw diagnostic content only
 | ||||
|         let mut hasher = DefaultHasher::new(); | ||||
| 
 | ||||
|         // Hash the length first to ensure different numbers of diagnostics produce different hashes
 | ||||
|         diagnostics.hash(&mut hasher); | ||||
| 
 | ||||
|         format!("{:x}", hasher.finish()) | ||||
|         Some(format!("{:x}", hasher.finish())) | ||||
|     } | ||||
| 
 | ||||
|     pub(super) fn result_id(&self) -> String { | ||||
|     /// Computes the result ID for the diagnostics.
 | ||||
|     ///
 | ||||
|     /// Returns `None` if there are no diagnostics.
 | ||||
|     pub(super) fn result_id(&self) -> Option<String> { | ||||
|         Self::result_id_from_hash(&self.items) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: CompletionParams, | ||||
|     ) -> crate::server::Result<Option<CompletionResponse>> { | ||||
|  |  | |||
|  | @ -29,11 +29,11 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: DocumentDiagnosticParams, | ||||
|     ) -> Result<DocumentDiagnosticReportResult> { | ||||
|         let diagnostics = compute_diagnostics(db, &snapshot); | ||||
|         let diagnostics = compute_diagnostics(db, snapshot); | ||||
| 
 | ||||
|         let Some(diagnostics) = diagnostics else { | ||||
|             return Ok(DocumentDiagnosticReportResult::Report( | ||||
|  | @ -43,23 +43,26 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler { | |||
| 
 | ||||
|         let result_id = diagnostics.result_id(); | ||||
| 
 | ||||
|         let report = if params.previous_result_id.as_deref() == Some(&result_id) { | ||||
|             DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport { | ||||
|                 related_documents: None, | ||||
|                 unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport { | ||||
|                     result_id, | ||||
|                 }, | ||||
|             }) | ||||
|         } else { | ||||
|             DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { | ||||
|                 related_documents: None, | ||||
|                 full_document_diagnostic_report: FullDocumentDiagnosticReport { | ||||
|                     result_id: Some(result_id), | ||||
|                     // SAFETY: Pull diagnostic requests are only called for text documents, not for
 | ||||
|                     // notebook documents.
 | ||||
|                     items: diagnostics.to_lsp_diagnostics(db).expect_text_document(), | ||||
|                 }, | ||||
|             }) | ||||
|         let report = match result_id { | ||||
|             Some(new_id) if Some(&new_id) == params.previous_result_id.as_ref() => { | ||||
|                 DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport { | ||||
|                     related_documents: None, | ||||
|                     unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport { | ||||
|                         result_id: new_id, | ||||
|                     }, | ||||
|                 }) | ||||
|             } | ||||
|             new_id => { | ||||
|                 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { | ||||
|                     related_documents: None, | ||||
|                     full_document_diagnostic_report: FullDocumentDiagnosticReport { | ||||
|                         result_id: new_id, | ||||
|                         // SAFETY: Pull diagnostic requests are only called for text documents, not for
 | ||||
|                         // notebook documents.
 | ||||
|                         items: diagnostics.to_lsp_diagnostics(db).expect_text_document(), | ||||
|                     }, | ||||
|                 }) | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         Ok(DocumentDiagnosticReportResult::Report(report)) | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: DocumentHighlightParams, | ||||
|     ) -> crate::server::Result<Option<Vec<DocumentHighlight>>> { | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: DocumentSymbolParams, | ||||
|     ) -> crate::server::Result<Option<lsp_types::DocumentSymbolResponse>> { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: GotoDeclarationParams, | ||||
|     ) -> crate::server::Result<Option<GotoDefinitionResponse>> { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: GotoDefinitionParams, | ||||
|     ) -> crate::server::Result<Option<GotoDefinitionResponse>> { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: ReferenceParams, | ||||
|     ) -> crate::server::Result<Option<Vec<Location>>> { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: GotoTypeDefinitionParams, | ||||
|     ) -> crate::server::Result<Option<GotoDefinitionResponse>> { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: HoverParams, | ||||
|     ) -> crate::server::Result<Option<lsp_types::Hover>> { | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: InlayHintParams, | ||||
|     ) -> crate::server::Result<Option<Vec<lsp_types::InlayHint>>> { | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: SelectionRangeParams, | ||||
|     ) -> crate::server::Result<Option<Vec<LspSelectionRange>>> { | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         _params: SemanticTokensParams, | ||||
|     ) -> crate::server::Result<Option<SemanticTokensResult>> { | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ impl BackgroundDocumentRequestHandler for SemanticTokensRangeRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: SemanticTokensRangeParams, | ||||
|     ) -> crate::server::Result<Option<SemanticTokensRangeResult>> { | ||||
|  |  | |||
|  | @ -2,6 +2,8 @@ use crate::Session; | |||
| use crate::server::api::traits::{RequestHandler, SyncRequestHandler}; | ||||
| use crate::session::client::Client; | ||||
| 
 | ||||
| use lsp_types::{WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult}; | ||||
| 
 | ||||
| pub(crate) struct ShutdownHandler; | ||||
| 
 | ||||
| impl RequestHandler for ShutdownHandler { | ||||
|  | @ -9,9 +11,23 @@ impl RequestHandler for ShutdownHandler { | |||
| } | ||||
| 
 | ||||
| impl SyncRequestHandler for ShutdownHandler { | ||||
|     fn run(session: &mut Session, _client: &Client, _params: ()) -> crate::server::Result<()> { | ||||
|         tracing::debug!("Received shutdown request, waiting for shutdown notification"); | ||||
|     fn run(session: &mut Session, client: &Client, _params: ()) -> crate::server::Result<()> { | ||||
|         tracing::debug!("Received shutdown request, waiting for exit notification"); | ||||
| 
 | ||||
|         // Respond to any pending workspace diagnostic requests
 | ||||
|         if let Some(suspended_workspace_request) = | ||||
|             session.take_suspended_workspace_diagnostic_request() | ||||
|         { | ||||
|             client.respond( | ||||
|                 &suspended_workspace_request.id, | ||||
|                 Ok(WorkspaceDiagnosticReportResult::Report( | ||||
|                     WorkspaceDiagnosticReport::default(), | ||||
|                 )), | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         session.set_shutdown_requested(true); | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { | |||
| 
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         _client: &Client, | ||||
|         params: SignatureHelpParams, | ||||
|     ) -> crate::server::Result<Option<SignatureHelp>> { | ||||
|  |  | |||
|  | @ -1,14 +1,15 @@ | |||
| use crate::PositionEncoding; | ||||
| 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::server::{Action, Result}; | ||||
| use crate::session::client::Client; | ||||
| use crate::session::index::Index; | ||||
| use crate::session::{SessionSnapshot, SuspendedWorkspaceDiagnosticRequest}; | ||||
| use crate::system::file_to_url; | ||||
| use lsp_server::RequestId; | ||||
| use lsp_types::request::WorkspaceDiagnosticRequest; | ||||
| use lsp_types::{ | ||||
|     FullDocumentDiagnosticReport, PreviousResultId, ProgressToken, | ||||
|  | @ -25,6 +26,70 @@ use std::sync::atomic::{AtomicUsize, Ordering}; | |||
| use std::time::Instant; | ||||
| use ty_project::{Db, ProgressReporter}; | ||||
| 
 | ||||
| /// Handler for [Workspace diagnostics](workspace-diagnostics)
 | ||||
| ///
 | ||||
| /// Workspace diagnostics are special in many ways compared to other request handlers.
 | ||||
| /// This is mostly due to the fact that computing them is expensive. Because of that,
 | ||||
| /// the LSP supports multiple optimizations of which we all make use:
 | ||||
| ///
 | ||||
| /// ## Partial results
 | ||||
| ///
 | ||||
| /// Many clients support partial results. They allow a server
 | ||||
| /// to send multiple responses (in the form of `$/progress` notifications) for
 | ||||
| /// the same request. We use partial results to stream the results for
 | ||||
| /// changed files. This has the obvious benefit is that users
 | ||||
| /// don't need to wait for the entire check to complete before they see any diagnostics.
 | ||||
| /// The other benefit of "chunking" the work also helps client to incrementally
 | ||||
| /// update (and repaint) the diagnostics instead of all at once.
 | ||||
| /// We did see lags in VS code for projects with 10k+ diagnostics before implementing
 | ||||
| /// this improvement.
 | ||||
| ///
 | ||||
| /// ## Result IDs
 | ||||
| ///
 | ||||
| /// The server can compute a result id for every file which the client
 | ||||
| /// sends back in the next pull or workspace diagnostic request. The way we use
 | ||||
| /// the result id is that we compute a fingerprint of the file's diagnostics (a hash)
 | ||||
| /// and compare it with the result id sent by the server. We know that
 | ||||
| /// the diagnostics for a file are unchanged (the client still has the most recent review)
 | ||||
| /// if the ids compare equal.
 | ||||
| ///
 | ||||
| /// Result IDs are also useful to identify files for which ty no longer emits
 | ||||
| /// any diagnostics. For example, file A contained a syntax error that has now been fixed
 | ||||
| /// by the user. The client will send us a result id for file A but we won't match it with
 | ||||
| /// any new diagnostics because all errors in the file were fixed. The fact that we can't
 | ||||
| /// match up the result ID tells us that we need to clear the diagnostics on the client
 | ||||
| /// side by sending an empty diagnostic report (report without any diagnostics). We'll set the
 | ||||
| /// result id to `None` so that the client stops sending us a result id for this file.
 | ||||
| ///
 | ||||
| /// Sending unchanged instead of the full diagnostics for files that haven't changed
 | ||||
| /// helps reduce the data that's sent from the server to the client and it also enables long-polling
 | ||||
| /// (see the next section).
 | ||||
| ///
 | ||||
| /// ## Long polling
 | ||||
| ///
 | ||||
| /// As of today (1st of August 2025), VS code's LSP client automatically schedules a
 | ||||
| /// workspace diagnostic request every two seconds because it doesn't know *when* to pull
 | ||||
| /// for new workspace diagnostics (it doesn't know what actions invalidate the diagnostics).
 | ||||
| /// However, running the workspace diagnostics every two seconds is wasting a lot of CPU cycles (and battery life as a result)
 | ||||
| /// if the user's only browsing the project (it requires ty to iterate over all files).
 | ||||
| /// That's why we implement long polling (as recommended in the LSP) for workspace diagnostics.
 | ||||
| ///
 | ||||
| /// The basic idea of long-polling is that the server doesn't respond if there are no diagnostics
 | ||||
| /// or all diagnostics are unchanged. Instead, the server keeps the request open (it doesn't respond)
 | ||||
| /// and only responses when the diagnostics change. This puts the server in full control of when
 | ||||
| /// to recheck a workspace and a client can simply wait for the response to come in.
 | ||||
| ///
 | ||||
| /// One challenge with long polling for ty's server architecture is that we can't just keep
 | ||||
| /// the background thread running because holding on to the [`ProjectDatabase`] references
 | ||||
| /// prevents notifications from acquiring the exclusive db lock (or the long polling background thread
 | ||||
| /// panics if a notification tries to do so). What we do instead is that this request handler
 | ||||
| /// doesn't send a response if there are no diagnostics or all are unchanged and it
 | ||||
| /// sets a "[snapshot](SuspendedWorkspaceDiagnosticRequest)" of the workspace diagnostic request on the [`Session`].
 | ||||
| /// The second part to this is in the notification request handling. ty retries the
 | ||||
| /// suspended workspace diagnostic request (if any) after every notification if the notification
 | ||||
| /// changed the [`Session`]'s state.
 | ||||
| ///
 | ||||
| /// [workspace-diagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_diagnostic)
 | ||||
| pub(crate) struct WorkspaceDiagnosticRequestHandler; | ||||
| 
 | ||||
| impl RequestHandler for WorkspaceDiagnosticRequestHandler { | ||||
|  | @ -33,7 +98,7 @@ impl RequestHandler for WorkspaceDiagnosticRequestHandler { | |||
| 
 | ||||
| impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { | ||||
|     fn run( | ||||
|         snapshot: SessionSnapshot, | ||||
|         snapshot: &SessionSnapshot, | ||||
|         client: &Client, | ||||
|         params: WorkspaceDiagnosticParams, | ||||
|     ) -> Result<WorkspaceDiagnosticReportResult> { | ||||
|  | @ -49,7 +114,7 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { | |||
|         let writer = ResponseWriter::new( | ||||
|             params.partial_result_params.partial_result_token, | ||||
|             params.previous_result_ids, | ||||
|             &snapshot, | ||||
|             snapshot, | ||||
|             client, | ||||
|         ); | ||||
| 
 | ||||
|  | @ -71,6 +136,50 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { | |||
| 
 | ||||
|         Ok(reporter.into_final_report()) | ||||
|     } | ||||
| 
 | ||||
|     fn handle_request( | ||||
|         id: &RequestId, | ||||
|         snapshot: SessionSnapshot, | ||||
|         client: &Client, | ||||
|         params: WorkspaceDiagnosticParams, | ||||
|     ) { | ||||
|         let result = Self::run(&snapshot, client, params.clone()); | ||||
| 
 | ||||
|         // Test if this is a no-op result, in which case we should long-poll the request and
 | ||||
|         // only respond once some diagnostics have changed to get the latest result ids.
 | ||||
|         //
 | ||||
|         // Bulk response: This the simple case. Simply test if all diagnostics are unchanged (or empty)
 | ||||
|         // Streaming: This trickier but follows the same principle.
 | ||||
|         // * If the server sent any partial results, then `result` is a `Partial` result (in which
 | ||||
|         //   case we shouldn't do any long polling because some diagnostics changed).
 | ||||
|         // * If this is a full report, then check if all items are unchanged (or empty), the same as for
 | ||||
|         //   the non-streaming case.
 | ||||
|         if let Ok(WorkspaceDiagnosticReportResult::Report(full)) = &result { | ||||
|             let all_unchanged = full | ||||
|                 .items | ||||
|                 .iter() | ||||
|                 .all(|item| matches!(item, WorkspaceDocumentDiagnosticReport::Unchanged(_))); | ||||
| 
 | ||||
|             if all_unchanged { | ||||
|                 tracing::debug!( | ||||
|                     "Suspending workspace diagnostic request, all diagnostics are unchanged or the project has no diagnostics" | ||||
|                 ); | ||||
| 
 | ||||
|                 client.queue_action(Action::SuspendWorkspaceDiagnostics(Box::new( | ||||
|                     SuspendedWorkspaceDiagnosticRequest { | ||||
|                         id: id.clone(), | ||||
|                         params: serde_json::to_value(¶ms).unwrap(), | ||||
|                         revision: snapshot.revision(), | ||||
|                     }, | ||||
|                 ))); | ||||
| 
 | ||||
|                 // Don't respond, keep the request open (long polling).
 | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         client.respond(id, result); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler { | ||||
|  | @ -147,7 +256,12 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { | |||
|             self.report_progress(); | ||||
|         } | ||||
| 
 | ||||
|         let mut response = self.response.lock().unwrap(); | ||||
|         // Another thread might have panicked at this point because of a salsa cancellation which
 | ||||
|         // poisoned the result. If the response is poisoned, just don't report and wait for our thread
 | ||||
|         // to unwind with a salsa cancellation next.
 | ||||
|         let Ok(mut response) = self.response.lock() else { | ||||
|             return; | ||||
|         }; | ||||
| 
 | ||||
|         // Don't report empty diagnostics. We clear previous diagnostics in `into_response`
 | ||||
|         // which also handles the case where a file no longer has diagnostics because
 | ||||
|  | @ -207,7 +321,7 @@ impl<'a> ResponseWriter<'a> { | |||
|                 token, | ||||
|                 is_test: snapshot.in_test(), | ||||
|                 last_flush: Instant::now(), | ||||
|                 batched: Vec::new(), | ||||
|                 changed: Vec::new(), | ||||
|                 unchanged: Vec::with_capacity(previous_result_ids.len()), | ||||
|             }) | ||||
|         } else { | ||||
|  | @ -242,35 +356,33 @@ impl<'a> ResponseWriter<'a> { | |||
| 
 | ||||
|         let result_id = Diagnostics::result_id_from_hash(diagnostics); | ||||
| 
 | ||||
|         let is_unchanged = self | ||||
|             .previous_result_ids | ||||
|             .remove(&url) | ||||
|             .is_some_and(|previous_result_id| previous_result_id == result_id); | ||||
|         let report = match result_id { | ||||
|             Some(new_id) if Some(&new_id) == self.previous_result_ids.remove(&url).as_ref() => { | ||||
|                 WorkspaceDocumentDiagnosticReport::Unchanged( | ||||
|                     WorkspaceUnchangedDocumentDiagnosticReport { | ||||
|                         uri: url, | ||||
|                         version, | ||||
|                         unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport { | ||||
|                             result_id: new_id, | ||||
|                         }, | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             new_id => { | ||||
|                 let lsp_diagnostics = diagnostics | ||||
|                     .iter() | ||||
|                     .map(|diagnostic| to_lsp_diagnostic(db, diagnostic, self.position_encoding)) | ||||
|                     .collect::<Vec<_>>(); | ||||
| 
 | ||||
|         let report = if is_unchanged { | ||||
|             WorkspaceDocumentDiagnosticReport::Unchanged( | ||||
|                 WorkspaceUnchangedDocumentDiagnosticReport { | ||||
|                 WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport { | ||||
|                     uri: url, | ||||
|                     version, | ||||
|                     unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport { | ||||
|                         result_id, | ||||
|                     full_document_diagnostic_report: FullDocumentDiagnosticReport { | ||||
|                         result_id: new_id, | ||||
|                         items: lsp_diagnostics, | ||||
|                     }, | ||||
|                 }, | ||||
|             ) | ||||
|         } else { | ||||
|             let lsp_diagnostics = diagnostics | ||||
|                 .iter() | ||||
|                 .map(|diagnostic| to_lsp_diagnostic(db, diagnostic, self.position_encoding)) | ||||
|                 .collect::<Vec<_>>(); | ||||
| 
 | ||||
|             WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport { | ||||
|                 uri: url, | ||||
|                 version, | ||||
|                 full_document_diagnostic_report: FullDocumentDiagnosticReport { | ||||
|                     result_id: Some(result_id), | ||||
|                     items: lsp_diagnostics, | ||||
|                 }, | ||||
|             }) | ||||
|                 }) | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         self.write_report(report); | ||||
|  | @ -306,7 +418,7 @@ impl<'a> ResponseWriter<'a> { | |||
| 
 | ||||
|         // Handle files that had diagnostics in previous request but no longer have any
 | ||||
|         // Any remaining entries in previous_results are files that were fixed
 | ||||
|         for previous_url in self.previous_result_ids.into_keys() { | ||||
|         for (previous_url, previous_result_id) in self.previous_result_ids { | ||||
|             // This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics
 | ||||
|             let version = self | ||||
|                 .index | ||||
|  | @ -315,22 +427,38 @@ impl<'a> ResponseWriter<'a> { | |||
|                 .and_then(|key| self.index.make_document_ref(key).ok()) | ||||
|                 .map(|doc| i64::from(doc.version())); | ||||
| 
 | ||||
|             items.push(WorkspaceDocumentDiagnosticReport::Full( | ||||
|                 WorkspaceFullDocumentDiagnosticReport { | ||||
|                     uri: previous_url, | ||||
|                     version, | ||||
|                     full_document_diagnostic_report: FullDocumentDiagnosticReport { | ||||
|                         result_id: None, // No result ID needed for empty diagnostics
 | ||||
|                         items: vec![],   // No diagnostics
 | ||||
|                     }, | ||||
|                 }, | ||||
|             )); | ||||
|             let new_result_id = Diagnostics::result_id_from_hash(&[]); | ||||
| 
 | ||||
|             let report = match new_result_id { | ||||
|                 Some(new_id) if new_id == previous_result_id => { | ||||
|                     WorkspaceDocumentDiagnosticReport::Unchanged( | ||||
|                         WorkspaceUnchangedDocumentDiagnosticReport { | ||||
|                             uri: previous_url, | ||||
|                             version, | ||||
|                             unchanged_document_diagnostic_report: | ||||
|                                 UnchangedDocumentDiagnosticReport { result_id: new_id }, | ||||
|                         }, | ||||
|                     ) | ||||
|                 } | ||||
|                 new_id => { | ||||
|                     WorkspaceDocumentDiagnosticReport::Full(WorkspaceFullDocumentDiagnosticReport { | ||||
|                         uri: previous_url, | ||||
|                         version, | ||||
|                         full_document_diagnostic_report: FullDocumentDiagnosticReport { | ||||
|                             result_id: new_id, | ||||
|                             items: vec![], // No diagnostics
 | ||||
|                         }, | ||||
|                     }) | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             items.push(report); | ||||
|         } | ||||
| 
 | ||||
|         match &mut self.mode { | ||||
|             ReportingMode::Streaming(streaming) => { | ||||
|                 items.extend( | ||||
|                     std::mem::take(&mut streaming.batched) | ||||
|                     std::mem::take(&mut streaming.changed) | ||||
|                         .into_iter() | ||||
|                         .map(WorkspaceDocumentDiagnosticReport::Full), | ||||
|                 ); | ||||
|  | @ -388,7 +516,7 @@ struct Streaming { | |||
|     /// The implementation uses batching to avoid too many
 | ||||
|     /// requests for large projects (can slow down the entire
 | ||||
|     /// analysis).
 | ||||
|     batched: Vec<WorkspaceFullDocumentDiagnosticReport>, | ||||
|     changed: Vec<WorkspaceFullDocumentDiagnosticReport>, | ||||
|     /// All the unchanged reports. Don't stream them,
 | ||||
|     /// since nothing has changed.
 | ||||
|     unchanged: Vec<WorkspaceUnchangedDocumentDiagnosticReport>, | ||||
|  | @ -398,7 +526,7 @@ impl Streaming { | |||
|     fn write_report(&mut self, report: WorkspaceDocumentDiagnosticReport) { | ||||
|         match report { | ||||
|             WorkspaceDocumentDiagnosticReport::Full(full) => { | ||||
|                 self.batched.push(full); | ||||
|                 self.changed.push(full); | ||||
|             } | ||||
|             WorkspaceDocumentDiagnosticReport::Unchanged(unchanged) => { | ||||
|                 self.unchanged.push(unchanged); | ||||
|  | @ -407,13 +535,13 @@ impl Streaming { | |||
|     } | ||||
| 
 | ||||
|     fn maybe_flush(&mut self) { | ||||
|         if self.batched.is_empty() { | ||||
|         if self.changed.is_empty() { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Flush every ~50ms or whenever we have two items and this is a test run.
 | ||||
|         let should_flush = if self.is_test { | ||||
|             self.batched.len() >= 2 | ||||
|             self.changed.len() >= 2 | ||||
|         } else { | ||||
|             self.last_flush.elapsed().as_millis() >= 50 | ||||
|         }; | ||||
|  | @ -422,7 +550,7 @@ impl Streaming { | |||
|         } | ||||
| 
 | ||||
|         let items = self | ||||
|             .batched | ||||
|             .changed | ||||
|             .drain(..) | ||||
|             .map(WorkspaceDocumentDiagnosticReport::Full) | ||||
|             .collect(); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ impl RequestHandler for WorkspaceSymbolRequestHandler { | |||
| 
 | ||||
| impl BackgroundRequestHandler for WorkspaceSymbolRequestHandler { | ||||
|     fn run( | ||||
|         snapshot: SessionSnapshot, | ||||
|         snapshot: &SessionSnapshot, | ||||
|         _client: &Client, | ||||
|         params: WorkspaceSymbolParams, | ||||
|     ) -> crate::server::Result<Option<WorkspaceSymbolResponse>> { | ||||
|  |  | |||
|  | @ -33,10 +33,10 @@ | |||
| //! See the `./requests` and `./notifications` directories for concrete implementations of these
 | ||||
| //! traits in action.
 | ||||
| 
 | ||||
| use std::borrow::Cow; | ||||
| 
 | ||||
| use crate::session::client::Client; | ||||
| use crate::session::{DocumentSnapshot, Session, SessionSnapshot}; | ||||
| use lsp_server::RequestId; | ||||
| use std::borrow::Cow; | ||||
| 
 | ||||
| use lsp_types::Url; | ||||
| use lsp_types::notification::Notification; | ||||
|  | @ -91,12 +91,36 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler { | |||
|         params: &<<Self as RequestHandler>::RequestType as Request>::Params, | ||||
|     ) -> Cow<Url>; | ||||
| 
 | ||||
|     /// Processes the request parameters and returns the LSP request result.
 | ||||
|     ///
 | ||||
|     /// This is the main method that handlers implement. It takes the request parameters
 | ||||
|     /// from the client and computes the appropriate response data for the LSP request.
 | ||||
|     fn run_with_snapshot( | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: &DocumentSnapshot, | ||||
|         client: &Client, | ||||
|         params: <<Self as RequestHandler>::RequestType as Request>::Params, | ||||
|     ) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>; | ||||
| 
 | ||||
|     /// Handles the entire request lifecycle and sends the response to the client.
 | ||||
|     ///
 | ||||
|     /// It allows handlers to customize how the server sends the response to the client.
 | ||||
|     fn handle_request( | ||||
|         id: &RequestId, | ||||
|         db: &ProjectDatabase, | ||||
|         snapshot: DocumentSnapshot, | ||||
|         client: &Client, | ||||
|         params: <<Self as RequestHandler>::RequestType as Request>::Params, | ||||
|     ) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>; | ||||
|     ) { | ||||
|         let result = Self::run_with_snapshot(db, &snapshot, client, params); | ||||
| 
 | ||||
|         if let Err(err) = &result { | ||||
|             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.respond(id, result); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A request handler that can be run on a background thread.
 | ||||
|  | @ -106,11 +130,34 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler { | |||
| /// operations that require access to the entire session state, such as fetching workspace
 | ||||
| /// diagnostics.
 | ||||
| pub(super) trait BackgroundRequestHandler: RetriableRequestHandler { | ||||
|     /// Processes the request parameters and returns the LSP request result.
 | ||||
|     ///
 | ||||
|     /// This is the main method that handlers implement. It takes the request parameters
 | ||||
|     /// from the client and computes the appropriate response data for the LSP request.
 | ||||
|     fn run( | ||||
|         snapshot: SessionSnapshot, | ||||
|         snapshot: &SessionSnapshot, | ||||
|         client: &Client, | ||||
|         params: <<Self as RequestHandler>::RequestType as Request>::Params, | ||||
|     ) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>; | ||||
| 
 | ||||
|     /// Handles the request lifecycle and sends the response to the client.
 | ||||
|     ///
 | ||||
|     /// It allows handlers to customize how the server sends the response to the client.
 | ||||
|     fn handle_request( | ||||
|         id: &RequestId, | ||||
|         snapshot: SessionSnapshot, | ||||
|         client: &Client, | ||||
|         params: <<Self as RequestHandler>::RequestType as Request>::Params, | ||||
|     ) { | ||||
|         let result = Self::run(&snapshot, client, params); | ||||
| 
 | ||||
|         if let Err(err) = &result { | ||||
|             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.respond(id, result); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A supertrait for any server notification handler.
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| use crate::server::schedule::Scheduler; | ||||
| use crate::server::{Server, api}; | ||||
| use crate::session::ClientOptions; | ||||
| use crate::session::client::{Client, ClientResponseHandler}; | ||||
| use crate::session::{ClientOptions, SuspendedWorkspaceDiagnosticRequest}; | ||||
| use anyhow::anyhow; | ||||
| use crossbeam::select; | ||||
| use lsp_server::Message; | ||||
|  | @ -49,7 +49,8 @@ impl Server { | |||
| 
 | ||||
|                             if self.session.is_shutdown_requested() { | ||||
|                                 tracing::warn!( | ||||
|                                     "Received request after server shutdown was requested, discarding" | ||||
|                                     "Received request `{}` after server shutdown was requested, discarding", | ||||
|                                     &req.method | ||||
|                                 ); | ||||
|                                 client.respond_err( | ||||
|                                     req.id, | ||||
|  | @ -130,7 +131,8 @@ impl Server { | |||
|                             .incoming() | ||||
|                             .is_pending(&request.id) | ||||
|                         { | ||||
|                             api::request(request); | ||||
|                             let task = api::request(request); | ||||
|                             scheduler.dispatch(task, &mut self.session, client); | ||||
|                         } else { | ||||
|                             tracing::debug!( | ||||
|                                 "Request {}/{} was cancelled, not retrying", | ||||
|  | @ -142,6 +144,13 @@ impl Server { | |||
| 
 | ||||
|                     Action::SendRequest(request) => client.send_request_raw(&self.session, request), | ||||
| 
 | ||||
|                     Action::SuspendWorkspaceDiagnostics(suspended_request) => { | ||||
|                         self.session.set_suspended_workspace_diagnostics_request( | ||||
|                             *suspended_request, | ||||
|                             &client, | ||||
|                         ); | ||||
|                     } | ||||
| 
 | ||||
|                     Action::InitializeWorkspaces(workspaces_with_options) => { | ||||
|                         self.session | ||||
|                             .initialize_workspaces(workspaces_with_options, &client); | ||||
|  | @ -304,6 +313,8 @@ pub(crate) enum Action { | |||
|     /// Send a request from the server to the client.
 | ||||
|     SendRequest(SendRequest), | ||||
| 
 | ||||
|     SuspendWorkspaceDiagnostics(Box<SuspendedWorkspaceDiagnosticRequest>), | ||||
| 
 | ||||
|     /// Initialize the workspace after the server received
 | ||||
|     /// the options from the client.
 | ||||
|     InitializeWorkspaces(Vec<(Url, ClientOptions)>), | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ pub(in crate::server) enum BackgroundSchedule { | |||
| /// while local tasks have exclusive access and can modify it as they please. Keep in mind that
 | ||||
| /// local tasks will **block** the main event loop, so only use local tasks if you **need**
 | ||||
| /// mutable state access or you need the absolute lowest latency possible.
 | ||||
| #[must_use] | ||||
| pub(in crate::server) enum Task { | ||||
|     Background(BackgroundTaskBuilder), | ||||
|     Sync(SyncTask), | ||||
|  |  | |||
|  | @ -2,9 +2,9 @@ | |||
| 
 | ||||
| use anyhow::{Context, anyhow}; | ||||
| use index::DocumentQueryError; | ||||
| use lsp_server::Message; | ||||
| use lsp_server::{Message, RequestId}; | ||||
| use lsp_types::notification::{Exit, Notification}; | ||||
| use lsp_types::request::{Request, Shutdown}; | ||||
| use lsp_types::request::{Request, Shutdown, WorkspaceDiagnosticRequest}; | ||||
| use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url}; | ||||
| use options::GlobalOptions; | ||||
| use ruff_db::Db; | ||||
|  | @ -24,7 +24,7 @@ pub(crate) use self::options::AllOptions; | |||
| pub use self::options::{ClientOptions, DiagnosticMode}; | ||||
| pub(crate) use self::settings::ClientSettings; | ||||
| use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; | ||||
| use crate::server::publish_settings_diagnostics; | ||||
| use crate::server::{Action, publish_settings_diagnostics}; | ||||
| use crate::session::client::Client; | ||||
| use crate::session::request_queue::RequestQueue; | ||||
| use crate::system::{AnySystemPath, LSPSystem}; | ||||
|  | @ -81,6 +81,16 @@ pub(crate) struct Session { | |||
|     in_test: bool, | ||||
| 
 | ||||
|     deferred_messages: VecDeque<Message>, | ||||
| 
 | ||||
|     /// A revision counter. It gets incremented on every change to `Session` that
 | ||||
|     /// could result in different workspace diagnostics.
 | ||||
|     revision: u64, | ||||
| 
 | ||||
|     /// A pending workspace diagnostics request because there were no diagnostics
 | ||||
|     /// or no changes when when the request ran last time.
 | ||||
|     /// We'll re-run the request after every change to `Session` (see `revision`)
 | ||||
|     /// to see if there are now changes and, if so, respond to the client.
 | ||||
|     suspended_workspace_diagnostics_request: Option<SuspendedWorkspaceDiagnosticRequest>, | ||||
| } | ||||
| 
 | ||||
| /// LSP State for a Project
 | ||||
|  | @ -137,6 +147,8 @@ impl Session { | |||
|             request_queue: RequestQueue::new(), | ||||
|             shutdown_requested: false, | ||||
|             in_test, | ||||
|             suspended_workspace_diagnostics_request: None, | ||||
|             revision: 0, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|  | @ -155,6 +167,56 @@ impl Session { | |||
|         self.shutdown_requested = requested; | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) fn set_suspended_workspace_diagnostics_request( | ||||
|         &mut self, | ||||
|         request: SuspendedWorkspaceDiagnosticRequest, | ||||
|         client: &Client, | ||||
|     ) { | ||||
|         self.suspended_workspace_diagnostics_request = Some(request); | ||||
|         // Run the suspended workspace diagnostic request immediately in case there
 | ||||
|         // were changes since the workspace diagnostics background thread queued
 | ||||
|         // the action to suspend the workspace diagnostic request.
 | ||||
|         self.resume_suspended_workspace_diagnostic_request(client); | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) fn take_suspended_workspace_diagnostic_request( | ||||
|         &mut self, | ||||
|     ) -> Option<SuspendedWorkspaceDiagnosticRequest> { | ||||
|         self.suspended_workspace_diagnostics_request.take() | ||||
|     } | ||||
| 
 | ||||
|     /// Resumes (retries) the workspace diagnostic request if there
 | ||||
|     /// were any changes to the [`Session`] (the revision got bumped)
 | ||||
|     /// since the workspace diagnostic request ran last time.
 | ||||
|     ///
 | ||||
|     /// The workspace diagnostic requests is ignored if the request
 | ||||
|     /// was cancelled in the meantime.
 | ||||
|     pub(crate) fn resume_suspended_workspace_diagnostic_request(&mut self, client: &Client) { | ||||
|         self.suspended_workspace_diagnostics_request = self | ||||
|             .suspended_workspace_diagnostics_request | ||||
|             .take() | ||||
|             .and_then(|request| { | ||||
|                 if !self.request_queue.incoming().is_pending(&request.id) { | ||||
|                     // Clear out the suspended request if the request has been cancelled.
 | ||||
|                     tracing::debug!("Skipping suspended workspace diagnostics request `{}` because it was cancelled", request.id); | ||||
|                     return None; | ||||
|                 } | ||||
| 
 | ||||
|                 request.resume_if_revision_changed(self.revision, client) | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /// Bumps the revision.
 | ||||
|     ///
 | ||||
|     /// The revision is used to track when workspace diagnostics may have changed and need to be re-run.
 | ||||
|     /// It's okay if a bump doesn't necessarily result in new workspace diagnostics.
 | ||||
|     ///
 | ||||
|     /// In general, any change to a project database should bump the revision and so should
 | ||||
|     /// any change to the document states (but also when the open workspaces change etc.).
 | ||||
|     fn bump_revision(&mut self) { | ||||
|         self.revision += 1; | ||||
|     } | ||||
| 
 | ||||
|     /// 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
 | ||||
|  | @ -318,6 +380,8 @@ impl Session { | |||
|                 .cloned() | ||||
|         }); | ||||
| 
 | ||||
|         self.bump_revision(); | ||||
| 
 | ||||
|         self.project_db_mut(path) | ||||
|             .apply_changes(changes, overrides.as_ref()) | ||||
|     } | ||||
|  | @ -465,6 +529,7 @@ impl Session { | |||
|             position_encoding: self.position_encoding, | ||||
|             in_test: self.in_test, | ||||
|             resolved_client_capabilities: self.resolved_client_capabilities, | ||||
|             revision: self.revision, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -483,12 +548,14 @@ impl Session { | |||
|         document: NotebookDocument, | ||||
|     ) { | ||||
|         self.index_mut().open_notebook_document(path, document); | ||||
|         self.bump_revision(); | ||||
|     } | ||||
| 
 | ||||
|     /// Registers a text document at the provided `path`.
 | ||||
|     /// If a document is already open here, it will be overwritten.
 | ||||
|     pub(crate) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) { | ||||
|         self.index_mut().open_text_document(path, document); | ||||
|         self.bump_revision(); | ||||
|     } | ||||
| 
 | ||||
|     /// Updates a text document at the associated `key`.
 | ||||
|  | @ -501,8 +568,14 @@ impl Session { | |||
|         new_version: DocumentVersion, | ||||
|     ) -> crate::Result<()> { | ||||
|         let position_encoding = self.position_encoding; | ||||
|         self.index_mut() | ||||
|             .update_text_document(key, content_changes, new_version, position_encoding) | ||||
|         self.index_mut().update_text_document( | ||||
|             key, | ||||
|             content_changes, | ||||
|             new_version, | ||||
|             position_encoding, | ||||
|         )?; | ||||
|         self.bump_revision(); | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// De-registers a document, specified by its key.
 | ||||
|  | @ -656,6 +729,7 @@ pub(crate) struct SessionSnapshot { | |||
|     position_encoding: PositionEncoding, | ||||
|     resolved_client_capabilities: ResolvedClientCapabilities, | ||||
|     in_test: bool, | ||||
|     revision: u64, | ||||
| 
 | ||||
|     /// IMPORTANT: It's important that the databases come last, or at least,
 | ||||
|     /// after any `Arc` that we try to extract or mutate in-place using `Arc::into_inner`
 | ||||
|  | @ -689,6 +763,10 @@ impl SessionSnapshot { | |||
|     pub(crate) const fn in_test(&self) -> bool { | ||||
|         self.in_test | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) fn revision(&self) -> u64 { | ||||
|         self.revision | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Default)] | ||||
|  | @ -847,3 +925,43 @@ impl DefaultProject { | |||
|         self.0.get_mut() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// A workspace diagnostic request that didn't yield any changes or diagnostic
 | ||||
| /// when it ran the last time.
 | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct SuspendedWorkspaceDiagnosticRequest { | ||||
|     /// The LSP request id
 | ||||
|     pub(crate) id: RequestId, | ||||
| 
 | ||||
|     /// The params passed to the `workspace/diagnostic` request.
 | ||||
|     pub(crate) params: serde_json::Value, | ||||
| 
 | ||||
|     /// The session's revision when the request ran the last time.
 | ||||
|     ///
 | ||||
|     /// This is to prevent races between:
 | ||||
|     /// * The background thread completes
 | ||||
|     /// * A did change notification coming in
 | ||||
|     /// * storing this struct on `Session`
 | ||||
|     ///
 | ||||
|     /// The revision helps us detect that a did change notification
 | ||||
|     /// happened in the meantime, so that we can reschedule the
 | ||||
|     /// workspace diagnostic request immediately.
 | ||||
|     pub(crate) revision: u64, | ||||
| } | ||||
| 
 | ||||
| impl SuspendedWorkspaceDiagnosticRequest { | ||||
|     fn resume_if_revision_changed(self, current_revision: u64, client: &Client) -> Option<Self> { | ||||
|         if self.revision == current_revision { | ||||
|             return Some(self); | ||||
|         } | ||||
| 
 | ||||
|         tracing::debug!("Resuming workspace diagnostics request after revision bump"); | ||||
|         client.queue_action(Action::RetryRequest(lsp_server::Request { | ||||
|             id: self.id, | ||||
|             method: WorkspaceDiagnosticRequest::METHOD.to_string(), | ||||
|             params: self.params, | ||||
|         })); | ||||
| 
 | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Micha Reiser
						Micha Reiser