diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index 9c536b2a31..cda81a9192 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -83,15 +83,20 @@ impl ProjectDatabase { /// Checks all open files in the project and its dependencies. pub fn check(&self) -> Vec { - let mut reporter = DummyReporter; - let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter); - self.project().check(self, reporter) + self.check_with_mode(CheckMode::OpenFiles) } /// Checks all open files in the project and its dependencies, using the given reporter. pub fn check_with_reporter(&self, reporter: &mut dyn Reporter) -> Vec { let reporter = AssertUnwindSafe(reporter); - self.project().check(self, reporter) + self.project().check(self, CheckMode::OpenFiles, reporter) + } + + /// Check the project with the given mode. + pub fn check_with_mode(&self, mode: CheckMode) -> Vec { + let mut reporter = DummyReporter; + let reporter = AssertUnwindSafe(&mut reporter as &mut dyn Reporter); + self.project().check(self, mode, reporter) } #[tracing::instrument(level = "debug", skip(self))] @@ -157,6 +162,17 @@ impl std::fmt::Debug for ProjectDatabase { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CheckMode { + /// Checks only the open files in the project. + OpenFiles, + + /// Checks all files in the project, ignoring the open file set. + /// + /// This includes virtual files, such as those created by the language server. + AllFiles, +} + /// Stores memory usage information. pub struct SalsaMemoryDump { total_fields: usize, diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 122f0010c9..e34afa0374 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -1,7 +1,7 @@ use crate::glob::{GlobFilterCheckMode, IncludeResult}; use crate::metadata::options::{OptionDiagnostic, ToSettingsError}; use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; -pub use db::{Db, ProjectDatabase, SalsaMemoryDump}; +pub use db::{CheckMode, Db, ProjectDatabase, SalsaMemoryDump}; use files::{Index, Indexed, IndexedFiles}; use metadata::settings::Settings; pub use metadata::{ProjectMetadata, ProjectMetadataError}; @@ -214,6 +214,7 @@ impl Project { pub(crate) fn check( self, db: &ProjectDatabase, + mode: CheckMode, mut reporter: AssertUnwindSafe<&mut dyn Reporter>, ) -> Vec { let project_span = tracing::debug_span!("Project::check"); @@ -228,7 +229,11 @@ impl Project { .map(OptionDiagnostic::to_diagnostic), ); - let files = ProjectFiles::new(db, self); + let files = match mode { + CheckMode::OpenFiles => ProjectFiles::new(db, self), + // TODO: Consider open virtual files as well + CheckMode::AllFiles => ProjectFiles::Indexed(self.files(db)), + }; reporter.set_files(files.len()); diagnostics.extend( diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 5db60c3674..01338053fa 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -173,6 +173,7 @@ impl Server { diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { identifier: Some(crate::DIAGNOSTIC_NAME.into()), inter_file_dependencies: true, + workspace_diagnostics: true, ..Default::default() })), text_document_sync: Some(TextDocumentSyncCapability::Options( diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index e15d6b98a3..132476cf87 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -33,6 +33,11 @@ pub(super) fn request(req: server::Request) -> Task { >( req, BackgroundSchedule::Worker ), + requests::WorkspaceDiagnosticRequestHandler::METHOD => background_request_task::< + requests::WorkspaceDiagnosticRequestHandler, + >( + req, BackgroundSchedule::Worker + ), requests::GotoTypeDefinitionRequestHandler::METHOD => background_document_request_task::< requests::GotoTypeDefinitionRequestHandler, >( @@ -135,7 +140,6 @@ where })) } -#[expect(dead_code)] fn background_request_task( req: server::Request, schedule: BackgroundSchedule, diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index 7a05ba8d76..c430e32f53 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -166,7 +166,7 @@ pub(super) fn compute_diagnostics( /// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP /// [`Diagnostic`]. -fn to_lsp_diagnostic( +pub(super) fn to_lsp_diagnostic( db: &dyn Db, diagnostic: &ruff_db::diagnostic::Diagnostic, encoding: PositionEncoding, diff --git a/crates/ty_server/src/server/api/notifications/did_close.rs b/crates/ty_server/src/server/api/notifications/did_close.rs index 168c27048a..e7c87364ae 100644 --- a/crates/ty_server/src/server/api/notifications/did_close.rs +++ b/crates/ty_server/src/server/api/notifications/did_close.rs @@ -41,7 +41,12 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler { ); } - clear_diagnostics(&key, client); + if !session.global_settings().diagnostic_mode().is_workspace() { + // The server needs to clear the diagnostics regardless of whether the client supports + // pull diagnostics or not. This is because the client only has the capability to fetch + // the diagnostics but does not automatically clear them when a document is closed. + clear_diagnostics(&key, client); + } Ok(()) } diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 12e7243893..6770b0e448 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -4,6 +4,7 @@ mod goto_type_definition; mod hover; mod inlay_hints; mod shutdown; +mod workspace_diagnostic; pub(super) use completion::CompletionRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; @@ -11,3 +12,4 @@ pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; pub(super) use inlay_hints::InlayHintRequestHandler; pub(super) use shutdown::ShutdownHandler; +pub(super) use workspace_diagnostic::WorkspaceDiagnosticRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs new file mode 100644 index 0000000000..cce5828b56 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -0,0 +1,108 @@ +use lsp_types::request::WorkspaceDiagnosticRequest; +use lsp_types::{ + FullDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, + WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport, + WorkspaceFullDocumentDiagnosticReport, +}; +use rustc_hash::FxHashMap; +use ty_project::CheckMode; + +use crate::server::Result; +use crate::server::api::diagnostics::to_lsp_diagnostic; +use crate::server::api::traits::{ + BackgroundRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::WorkspaceSnapshot; +use crate::session::client::Client; +use crate::system::file_to_url; + +pub(crate) struct WorkspaceDiagnosticRequestHandler; + +impl RequestHandler for WorkspaceDiagnosticRequestHandler { + type RequestType = WorkspaceDiagnosticRequest; +} + +impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler { + fn run( + snapshot: WorkspaceSnapshot, + _client: &Client, + _params: WorkspaceDiagnosticParams, + ) -> Result { + let index = snapshot.index(); + + if !index.global_settings().diagnostic_mode().is_workspace() { + tracing::debug!("Workspace diagnostics is disabled; returning empty report"); + return Ok(WorkspaceDiagnosticReportResult::Report( + WorkspaceDiagnosticReport { items: vec![] }, + )); + } + + let mut items = Vec::new(); + + for db in snapshot.projects() { + let diagnostics = db.check_with_mode(CheckMode::AllFiles); + + // Group diagnostics by URL + let mut diagnostics_by_url: FxHashMap> = FxHashMap::default(); + + for diagnostic in diagnostics { + if let Some(span) = diagnostic.primary_span() { + let file = span.expect_ty_file(); + let Some(url) = file_to_url(db, file) else { + tracing::debug!("Failed to convert file to URL at {}", file.path(db)); + continue; + }; + diagnostics_by_url.entry(url).or_default().push(diagnostic); + } + } + + items.reserve(diagnostics_by_url.len()); + + // Convert to workspace diagnostic report format + for (url, file_diagnostics) in diagnostics_by_url { + let version = index + .key_from_url(url.clone()) + .ok() + .and_then(|key| index.make_document_ref(&key)) + .map(|doc| i64::from(doc.version())); + + // Convert diagnostics to LSP format + let lsp_diagnostics = file_diagnostics + .into_iter() + .map(|diagnostic| { + to_lsp_diagnostic(db, &diagnostic, snapshot.position_encoding()) + }) + .collect::>(); + + items.push(WorkspaceDocumentDiagnosticReport::Full( + WorkspaceFullDocumentDiagnosticReport { + uri: url, + version, + full_document_diagnostic_report: FullDocumentDiagnosticReport { + // TODO: We don't implement result ID caching yet + result_id: None, + items: lsp_diagnostics, + }, + }, + )); + } + } + + Ok(WorkspaceDiagnosticReportResult::Report( + WorkspaceDiagnosticReport { items }, + )) + } +} + +impl RetriableRequestHandler for WorkspaceDiagnosticRequestHandler { + fn salsa_cancellation_error() -> lsp_server::ResponseError { + lsp_server::ResponseError { + code: lsp_server::ErrorCode::ServerCancelled as i32, + message: "server cancelled the request".to_owned(), + data: serde_json::to_value(lsp_types::DiagnosticServerCancellationData { + retrigger_request: true, + }) + .ok(), + } + } +} diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 8453f7348c..0d595dac66 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -383,6 +383,10 @@ impl Session { pub(crate) fn client_capabilities(&self) -> &ResolvedClientCapabilities { &self.resolved_client_capabilities } + + pub(crate) fn global_settings(&self) -> Arc { + self.index().global_settings() + } } /// A guard that holds the only reference to the index and allows modifying it. @@ -469,7 +473,6 @@ pub(crate) struct WorkspaceSnapshot { position_encoding: PositionEncoding, } -#[expect(dead_code)] impl WorkspaceSnapshot { pub(crate) fn projects(&self) -> &[ProjectDatabase] { &self.projects diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs index a984b4e34e..f77d6c437d 100644 --- a/crates/ty_server/src/session/options.rs +++ b/crates/ty_server/src/session/options.rs @@ -46,6 +46,26 @@ pub(crate) struct ClientOptions { /// Settings under the `python.*` namespace in VS Code that are useful for the ty language /// server. python: Option, + /// Diagnostic mode for the language server. + diagnostic_mode: Option, +} + +/// Diagnostic mode for the language server. +#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) enum DiagnosticMode { + /// Check only currently open files. + #[default] + OpenFilesOnly, + /// Check all files in the workspace. + Workspace, +} + +impl DiagnosticMode { + pub(crate) fn is_workspace(self) -> bool { + matches!(self, DiagnosticMode::Workspace) + } } impl ClientOptions { @@ -57,6 +77,7 @@ impl ClientOptions { .and_then(|python| python.ty) .and_then(|ty| ty.disable_language_services) .unwrap_or_default(), + diagnostic_mode: self.diagnostic_mode.unwrap_or_default(), } } } diff --git a/crates/ty_server/src/session/settings.rs b/crates/ty_server/src/session/settings.rs index aba23a5c14..f24cd92350 100644 --- a/crates/ty_server/src/session/settings.rs +++ b/crates/ty_server/src/session/settings.rs @@ -1,3 +1,5 @@ +use super::options::DiagnosticMode; + /// Resolved client settings for a specific document. These settings are meant to be /// used directly by the server, and are *not* a 1:1 representation with how the client /// sends them. @@ -5,10 +7,15 @@ #[cfg_attr(test, derive(PartialEq, Eq))] pub(crate) struct ClientSettings { pub(super) disable_language_services: bool, + pub(super) diagnostic_mode: DiagnosticMode, } impl ClientSettings { pub(crate) fn is_language_services_disabled(&self) -> bool { self.disable_language_services } + + pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode { + self.diagnostic_mode + } }