Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions crates/ty_project/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,20 @@ impl ProjectDatabase {

/// Checks all open files in the project and its dependencies.
pub fn check(&self) -> Vec<Diagnostic> {
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<Diagnostic> {
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<Diagnostic> {
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))]
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions crates/ty_project/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -214,6 +214,7 @@ impl Project {
pub(crate) fn check(
self,
db: &ProjectDatabase,
mode: CheckMode,
mut reporter: AssertUnwindSafe<&mut dyn Reporter>,
) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check");
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions crates/ty_server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion crates/ty_server/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
>(
Expand Down Expand Up @@ -135,7 +140,6 @@ where
}))
}

#[expect(dead_code)]
fn background_request_task<R: traits::BackgroundRequestHandler>(
req: server::Request,
schedule: BackgroundSchedule,
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_server/src/server/api/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this doesn't need to be done when in workspace diagnostic mode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in workspace mode all diagnostics should be visible regardless of whether a document is open or close in an editor. So, we shouldn't clear the diagnostics when a user closes a file in the editor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhh makes sense of course.

}

Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions crates/ty_server/src/server/api/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ mod goto_type_definition;
mod hover;
mod inlay_hints;
mod shutdown;
mod workspace_diagnostic;

pub(super) use completion::CompletionRequestHandler;
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
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;
108 changes: 108 additions & 0 deletions crates/ty_server/src/server/api/requests/workspace_diagnostic.rs
Original file line number Diff line number Diff line change
@@ -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<WorkspaceDiagnosticReportResult> {
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<Url, Vec<_>> = 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::<Vec<_>>();

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(),
}
}
}
5 changes: 4 additions & 1 deletion crates/ty_server/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ impl Session {
pub(crate) fn client_capabilities(&self) -> &ResolvedClientCapabilities {
&self.resolved_client_capabilities
}

pub(crate) fn global_settings(&self) -> Arc<ClientSettings> {
self.index().global_settings()
}
}

/// A guard that holds the only reference to the index and allows modifying it.
Expand Down Expand Up @@ -469,7 +473,6 @@ pub(crate) struct WorkspaceSnapshot {
position_encoding: PositionEncoding,
}

#[expect(dead_code)]
impl WorkspaceSnapshot {
pub(crate) fn projects(&self) -> &[ProjectDatabase] {
&self.projects
Expand Down
21 changes: 21 additions & 0 deletions crates/ty_server/src/session/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Python>,
/// Diagnostic mode for the language server.
diagnostic_mode: Option<DiagnosticMode>,
}

/// 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 {
Expand All @@ -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(),
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/ty_server/src/session/settings.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
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.
#[derive(Clone, Debug)]
#[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
}
}
Loading