Skip to content

Commit 4ed53ac

Browse files
committed
[ty] Support publishing diagnostics in the server
1 parent d95b029 commit 4ed53ac

File tree

10 files changed

+172
-34
lines changed

10 files changed

+172
-34
lines changed

crates/ty_server/src/document.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ impl DocumentKey {
5555
| DocumentKey::Text(url) => url,
5656
}
5757
}
58+
59+
/// Converts the key back into its original URL.
60+
pub(crate) fn into_url(self) -> Url {
61+
match self {
62+
DocumentKey::NotebookCell(url)
63+
| DocumentKey::Notebook(url)
64+
| DocumentKey::Text(url) => url,
65+
}
66+
}
5867
}
5968

6069
impl std::fmt::Display for DocumentKey {

crates/ty_server/src/document/notebook.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ impl NotebookDocument {
199199
}
200200

201201
/// Get the URI for a cell by its index within the cell array.
202-
#[expect(dead_code)]
203202
pub(crate) fn cell_uri_by_index(&self, index: CellId) -> Option<&lsp_types::Url> {
204203
self.cells.get(index).map(|cell| &cell.url)
205204
}

crates/ty_server/src/server/api/diagnostics.rs

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
use lsp_server::ErrorCode;
22
use lsp_types::notification::PublishDiagnostics;
33
use lsp_types::{
4-
Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, NumberOrString,
5-
PublishDiagnosticsParams, Range, Url,
4+
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
5+
NumberOrString, PublishDiagnosticsParams, Range, Url,
66
};
7+
use rustc_hash::FxHashMap;
78

89
use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic};
910
use ruff_db::files::FileRange;
1011
use ruff_db::source::{line_index, source_text};
1112
use ty_project::{Db, ProjectDatabase};
1213

13-
use crate::DocumentSnapshot;
14-
use crate::PositionEncoding;
1514
use crate::document::{FileRangeExt, ToRangeExt};
1615
use crate::server::Result;
1716
use crate::server::client::Notifier;
17+
use crate::{DocumentSnapshot, PositionEncoding};
1818

1919
use super::LSPResult;
2020

21+
/// A series of diagnostics across a single text document or an arbitrary number of notebook cells.
22+
pub(super) type DiagnosticsMap = FxHashMap<Url, Vec<Diagnostic>>;
23+
24+
/// Clears the diagnostics for the document at `uri`.
25+
///
26+
/// This is done by notifying the client with an empty list of diagnostics for the document.
2127
pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> {
2228
notifier
2329
.notify::<PublishDiagnostics>(PublishDiagnosticsParams {
@@ -29,31 +35,91 @@ pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> {
2935
Ok(())
3036
}
3137

38+
/// Publishes the diagnostics for the given document snapshot using the [publish diagnostics
39+
/// notification].
40+
///
41+
/// [publish diagnostics notification]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics
42+
pub(super) fn publish_diagnostics_for_document(
43+
db: &ProjectDatabase,
44+
snapshot: &DocumentSnapshot,
45+
notifier: &Notifier,
46+
) -> Result<()> {
47+
for (uri, diagnostics) in compute_diagnostics(db, snapshot) {
48+
notifier
49+
.notify::<PublishDiagnostics>(PublishDiagnosticsParams {
50+
uri,
51+
diagnostics,
52+
version: Some(snapshot.query().version()),
53+
})
54+
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
55+
}
56+
57+
Ok(())
58+
}
59+
3260
pub(super) fn compute_diagnostics(
3361
db: &ProjectDatabase,
3462
snapshot: &DocumentSnapshot,
35-
) -> Vec<Diagnostic> {
63+
) -> DiagnosticsMap {
3664
let Some(file) = snapshot.file(db) else {
3765
tracing::info!(
3866
"No file found for snapshot for `{}`",
3967
snapshot.query().file_url()
4068
);
41-
return vec![];
69+
return DiagnosticsMap::default();
4270
};
4371

4472
let diagnostics = match db.check_file(file) {
4573
Ok(diagnostics) => diagnostics,
4674
Err(cancelled) => {
4775
tracing::info!("Diagnostics computation {cancelled}");
48-
return vec![];
76+
return DiagnosticsMap::default();
4977
}
5078
};
5179

52-
diagnostics
53-
.as_slice()
54-
.iter()
55-
.map(|message| to_lsp_diagnostic(db, message, snapshot.encoding()))
56-
.collect()
80+
let mut diagnostics_map = DiagnosticsMap::default();
81+
let query = snapshot.query();
82+
// let source_kind = query.make_source_kind();
83+
84+
// Populates all relevant URLs with an empty diagnostic list.
85+
// This ensures that documents without diagnostics still get updated.
86+
if let Some(notebook) = query.as_notebook() {
87+
for url in notebook.urls() {
88+
diagnostics_map.entry(url.clone()).or_default();
89+
}
90+
} else {
91+
diagnostics_map
92+
.entry(query.make_key().into_url())
93+
.or_default();
94+
}
95+
96+
let lsp_diagnostics = diagnostics.as_slice().iter().map(|diagnostic| {
97+
(
98+
// TODO: Use the cell index instead using `source_kind`
99+
usize::default(),
100+
to_lsp_diagnostic(db, diagnostic, snapshot.encoding()),
101+
)
102+
});
103+
104+
if let Some(notebook) = query.as_notebook() {
105+
for (index, diagnostic) in lsp_diagnostics {
106+
let Some(uri) = notebook.cell_uri_by_index(index) else {
107+
tracing::warn!("Unable to find notebook cell at index {index}");
108+
continue;
109+
};
110+
diagnostics_map
111+
.entry(uri.clone())
112+
.or_default()
113+
.push(diagnostic);
114+
}
115+
} else {
116+
diagnostics_map
117+
.entry(query.make_key().into_url())
118+
.or_default()
119+
.extend(lsp_diagnostics.map(|(_, diagnostic)| diagnostic));
120+
}
121+
122+
diagnostics_map
57123
}
58124

59125
/// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP
@@ -97,9 +163,8 @@ fn to_lsp_diagnostic(
97163
.id()
98164
.is_lint()
99165
.then(|| {
100-
Some(lsp_types::CodeDescription {
101-
href: lsp_types::Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id()))
102-
.ok()?,
166+
Some(CodeDescription {
167+
href: Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id())).ok()?,
103168
})
104169
})
105170
.flatten();

crates/ty_server/src/server/api/notifications/did_change.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use lsp_server::ErrorCode;
2-
use lsp_types::DidChangeTextDocumentParams;
32
use lsp_types::notification::DidChangeTextDocument;
3+
use lsp_types::{DidChangeTextDocumentParams, VersionedTextDocumentIdentifier};
44

55
use ty_project::watch::ChangeEvent;
66

77
use crate::server::Result;
88
use crate::server::api::LSPResult;
9+
use crate::server::api::diagnostics::publish_diagnostics_for_document;
910
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
1011
use crate::server::client::{Notifier, Requester};
1112
use crate::session::Session;
@@ -20,21 +21,26 @@ impl NotificationHandler for DidChangeTextDocumentHandler {
2021
impl SyncNotificationHandler for DidChangeTextDocumentHandler {
2122
fn run(
2223
session: &mut Session,
23-
_notifier: Notifier,
24+
notifier: Notifier,
2425
_requester: &mut Requester,
2526
params: DidChangeTextDocumentParams,
2627
) -> Result<()> {
27-
let Ok(path) = url_to_any_system_path(&params.text_document.uri) else {
28+
let DidChangeTextDocumentParams {
29+
text_document: VersionedTextDocumentIdentifier { uri, version },
30+
content_changes,
31+
} = params;
32+
33+
let Ok(path) = url_to_any_system_path(&uri) else {
2834
return Ok(());
2935
};
3036

31-
let key = session.key_from_url(params.text_document.uri);
37+
let key = session.key_from_url(uri.clone());
3238

3339
session
34-
.update_text_document(&key, params.content_changes, params.text_document.version)
40+
.update_text_document(&key, content_changes, version)
3541
.with_failure_code(ErrorCode::InternalError)?;
3642

37-
match path {
43+
match path.clone() {
3844
AnySystemPath::System(path) => {
3945
let db = match session.project_db_for_path_mut(path.as_std_path()) {
4046
Some(db) => db,
@@ -48,7 +54,20 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
4854
}
4955
}
5056

51-
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
57+
// Publish diagnostics if the client doesn't support pull diagnostics
58+
if !session.resolved_client_capabilities().pull_diagnostics {
59+
let db = path
60+
.as_system()
61+
.and_then(|path| session.project_db_for_path(path.as_std_path()))
62+
.unwrap_or_else(|| session.default_project_db());
63+
let snapshot = session
64+
.take_snapshot(uri.clone())
65+
.ok_or_else(|| {
66+
anyhow::anyhow!("Unable to take snapshot for document with URL {uri}")
67+
})
68+
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
69+
publish_diagnostics_for_document(db, &snapshot, &notifier)?;
70+
}
5271

5372
Ok(())
5473
}

crates/ty_server/src/server/api/notifications/did_open.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use ty_project::watch::ChangeEvent;
66

77
use crate::TextDocument;
88
use crate::server::Result;
9+
use crate::server::api::LSPResult;
10+
use crate::server::api::diagnostics::publish_diagnostics_for_document;
911
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
1012
use crate::server::client::{Notifier, Requester};
1113
use crate::session::Session;
@@ -20,7 +22,7 @@ impl NotificationHandler for DidOpenTextDocumentHandler {
2022
impl SyncNotificationHandler for DidOpenTextDocumentHandler {
2123
fn run(
2224
session: &mut Session,
23-
_notifier: Notifier,
25+
notifier: Notifier,
2426
_requester: &mut Requester,
2527
DidOpenTextDocumentParams {
2628
text_document:
@@ -37,23 +39,36 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
3739
};
3840

3941
let document = TextDocument::new(text, version).with_language_id(&language_id);
40-
session.open_text_document(uri, document);
42+
session.open_text_document(uri.clone(), document);
4143

42-
match path {
44+
match &path {
4345
AnySystemPath::System(path) => {
4446
let db = match session.project_db_for_path_mut(path.as_std_path()) {
4547
Some(db) => db,
4648
None => session.default_project_db_mut(),
4749
};
48-
db.apply_changes(vec![ChangeEvent::Opened(path)], None);
50+
db.apply_changes(vec![ChangeEvent::Opened(path.clone())], None);
4951
}
5052
AnySystemPath::SystemVirtual(virtual_path) => {
5153
let db = session.default_project_db_mut();
52-
db.files().virtual_file(db, &virtual_path);
54+
db.files().virtual_file(db, virtual_path);
5355
}
5456
}
5557

56-
// TODO(dhruvmanila): Publish diagnostics if the client doesn't support pull diagnostics
58+
// Publish diagnostics if the client doesn't support pull diagnostics
59+
if !session.resolved_client_capabilities().pull_diagnostics {
60+
let db = path
61+
.as_system()
62+
.and_then(|path| session.project_db_for_path(path.as_std_path()))
63+
.unwrap_or_else(|| session.default_project_db());
64+
let snapshot = session
65+
.take_snapshot(uri.clone())
66+
.ok_or_else(|| {
67+
anyhow::anyhow!("Unable to take snapshot for document with URL {uri}")
68+
})
69+
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
70+
publish_diagnostics_for_document(db, &snapshot, &notifier)?;
71+
}
5772

5873
Ok(())
5974
}

crates/ty_server/src/server/api/requests/diagnostic.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
2929
_notifier: Notifier,
3030
_params: DocumentDiagnosticParams,
3131
) -> Result<DocumentDiagnosticReportResult> {
32-
let diagnostics = compute_diagnostics(db, &snapshot);
33-
3432
Ok(DocumentDiagnosticReportResult::Report(
3533
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
3634
related_documents: None,
3735
full_document_diagnostic_report: FullDocumentDiagnosticReport {
3836
result_id: None,
39-
items: diagnostics,
37+
// Pull diagnostic requests are only called for text documents.
38+
// Since diagnostic requests generate
39+
items: compute_diagnostics(db, &snapshot)
40+
.into_iter()
41+
.next()
42+
.map(|(_, diagnostics)| diagnostics)
43+
.unwrap_or_default(),
4044
},
4145
}),
4246
))

crates/ty_server/src/session.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ impl Session {
8686
})
8787
}
8888

89+
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
90+
&self.resolved_client_capabilities
91+
}
92+
8993
// TODO(dhruvmanila): Ideally, we should have a single method for `workspace_db_for_path_mut`
9094
// and `default_workspace_db_mut` but the borrow checker doesn't allow that.
9195
// https://github.com/astral-sh/ruff/pull/13041#discussion_r1726725437

crates/ty_server/src/session/capabilities.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ pub(crate) struct ResolvedClientCapabilities {
88
pub(crate) document_changes: bool,
99
pub(crate) diagnostics_refresh: bool,
1010
pub(crate) inlay_refresh: bool,
11+
12+
/// Whether [pull diagnostics] is supported.
13+
///
14+
/// [pull diagnostics]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics
1115
pub(crate) pull_diagnostics: bool,
16+
1217
/// Whether `textDocument.typeDefinition.linkSupport` is `true`
1318
pub(crate) type_definition_link_support: bool,
1419

crates/ty_server/src/session/index.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ pub enum DocumentQuery {
265265

266266
impl DocumentQuery {
267267
/// Retrieve the original key that describes this document query.
268-
#[expect(dead_code)]
269268
pub(crate) fn make_key(&self) -> DocumentKey {
270269
match self {
271270
Self::Text { file_url, .. } => DocumentKey::Text(file_url.clone()),
@@ -300,6 +299,16 @@ impl DocumentQuery {
300299
}
301300
}
302301

302+
// /// Generate a source kind used by the linter.
303+
// pub(crate) fn make_source_kind(&self) -> SourceKind {
304+
// match self {
305+
// Self::Text { document, .. } => SourceKind::Python(document.contents().to_string()),
306+
// Self::Notebook { notebook, .. } => {
307+
// SourceKind::ipy_notebook(notebook.make_ruff_notebook())
308+
// }
309+
// }
310+
// }
311+
303312
/// Attempt to access the single inner text document selected by the query.
304313
/// If this query is selecting an entire notebook document, this will return `None`.
305314
#[expect(dead_code)]

crates/ty_server/src/system.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,21 @@ pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option<Url> {
4747
}
4848

4949
/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`].
50-
#[derive(Debug)]
50+
#[derive(Clone, Debug)]
5151
pub(crate) enum AnySystemPath {
5252
System(SystemPathBuf),
5353
SystemVirtual(SystemVirtualPathBuf),
5454
}
5555

56+
impl AnySystemPath {
57+
pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> {
58+
match self {
59+
AnySystemPath::System(system_path_buf) => Some(system_path_buf),
60+
AnySystemPath::SystemVirtual(_) => None,
61+
}
62+
}
63+
}
64+
5665
#[derive(Debug)]
5766
pub(crate) struct LSPSystem {
5867
/// A read-only copy of the index where the server stores all the open documents and settings.

0 commit comments

Comments
 (0)