diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 93f618c09..dcbb64100 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -329,6 +329,7 @@ pub enum LanguageServerFeature { WorkspaceSymbols, // Symbols, use bitflags, see above? Diagnostics, + PullDiagnostics, RenameSymbol, InlayHints, } @@ -352,6 +353,7 @@ impl Display for LanguageServerFeature { DocumentSymbols => "document-symbols", WorkspaceSymbols => "workspace-symbols", Diagnostics => "diagnostics", + PullDiagnostics => "pull-diagnostics", RenameSymbol => "rename-symbol", InlayHints => "inlay-hints", }; diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 643aa9a26..a730a9bc0 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -18,7 +18,7 @@ use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; use std::sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{self, AtomicBool, AtomicU64, Ordering}, Arc, }; use std::{collections::HashMap, path::PathBuf}; @@ -60,6 +60,7 @@ pub struct Client { initialize_notify: Arc, /// workspace folders added while the server is still initializing req_timeout: u64, + supports_publish_diagnostic: AtomicBool, } impl Client { @@ -147,6 +148,17 @@ impl Client { } } + pub fn set_publish_diagnostic(&self, val: bool) { + self.supports_publish_diagnostic + .fetch_or(val, atomic::Ordering::Relaxed); + } + + /// Whether the server supports Publish Diagnostic + pub fn publish_diagnostic(&self) -> bool { + self.supports_publish_diagnostic + .load(atomic::Ordering::Relaxed) + } + fn add_workspace_folder( &self, root_uri: Option, @@ -232,6 +244,7 @@ impl Client { root_uri, workspace_folders: Mutex::new(workspace_folders), initialize_notify: initialize_notify.clone(), + supports_publish_diagnostic: AtomicBool::new(false), }; Ok((client, server_rx, initialize_notify)) @@ -346,6 +359,7 @@ impl Client { Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::Diagnostics => true, // there's no extra server capability + LanguageServerFeature::PullDiagnostics => capabilities.diagnostic_provider.is_some(), LanguageServerFeature::RenameSymbol => matches!( capabilities.rename_provider, Some(OneOf::Left(true)) | Some(OneOf::Right(_)) @@ -648,6 +662,10 @@ impl Client { }), ..Default::default() }), + diagnostic: Some(lsp::DiagnosticClientCapabilities { + dynamic_registration: Some(false), + related_document_support: Some(true), + }), publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities { version_support: Some(true), tag_support: Some(lsp::TagSupport { @@ -1224,6 +1242,32 @@ impl Client { }) } + pub fn text_document_diagnostic( + &self, + text_document: lsp::TextDocumentIdentifier, + previous_result_id: Option, + ) -> Option>> { + let capabilities = self.capabilities.get().unwrap(); + + // Return early if the server does not support pull diagnostic. + let identifier = match capabilities.diagnostic_provider.as_ref()? { + lsp::DiagnosticServerCapabilities::Options(cap) => cap.identifier.clone(), + lsp::DiagnosticServerCapabilities::RegistrationOptions(cap) => { + cap.diagnostic_options.identifier.clone() + } + }; + + let params = lsp::DocumentDiagnosticParams { + text_document, + identifier, + previous_result_id, + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }; + + Some(self.call::(params)) + } + pub fn text_document_document_highlight( &self, text_document: lsp::TextDocumentIdentifier, diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 3b9efb431..39e89f6c0 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -33,7 +33,7 @@ use crate::{ use std::{ cmp::Ordering, - collections::{BTreeMap, HashSet}, + collections::{btree_map::Entry, BTreeMap, HashMap, HashSet}, fmt::Write, future::Future, path::Path, @@ -1423,3 +1423,133 @@ fn compute_inlay_hints_for_view( Some(callback) } + +pub fn pull_diagnostic_for_current_doc(editor: &Editor, jobs: &mut crate::job::Jobs) { + let doc = doc!(editor); + let Some(language_server) = doc + .language_servers_with_feature(LanguageServerFeature::PullDiagnostics) + .next() + else { + return; + }; + // Specialization does not say whether it is possible to have both types of diagnostics. + // Assume we should prefer PublishDiagnostic if possible + if language_server.publish_diagnostic() { + return; + } + + let future = language_server + .text_document_diagnostic(doc.identifier(), doc.previous_diagnostic_id.clone()); + + let server_id = language_server.id(); + let original_path = doc + .path() + .expect("safety: the file has a path if there is a running language server") + .to_owned(); + + let callback = super::make_job_callback( + future.expect("safety: language server supports pull diagnostics"), + move |editor, _compositor, response: Option| { + let doc = match editor.document_by_path_mut(&original_path) { + Some(doc) => doc, + None => return, + }; + let Some(language_server) = doc.language_servers().find(|ls| ls.id() == server_id) + else { + return; + }; + // Pass them separately to satisfy borrow-checker + let offset_encoding = language_server.offset_encoding(); + let server_id = language_server.id(); + + let parse_diagnostic = |editor: &mut Editor, + path, + report: Vec, + result_id: Option| { + if let Some(doc) = editor.document_by_path_mut(&path) { + let diagnostics: Vec = report + .iter() + .map(|d| { + Document::lsp_diagnostic_to_diagnostic( + doc.text(), + doc.language_config(), + d, + server_id, + offset_encoding, + ) + .unwrap() + }) + .collect(); + + doc.previous_diagnostic_id = result_id; + // TODO: Should i get unchanged_sources? + doc.replace_diagnostics(diagnostics, &[], Some(server_id)); + } + let uri = helix_core::Uri::try_from(path).unwrap(); + + // TODO: Maybe share code with application.rs:802 + let mut diagnostics = report.into_iter().map(|d| (d, server_id)).collect(); + match editor.diagnostics.entry(uri) { + Entry::Occupied(o) => { + let current_diagnostics = o.into_mut(); + // there may entries of other language servers, which is why we can't overwrite the whole entry + current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id); + current_diagnostics.append(&mut diagnostics); + // Sort diagnostics first by severity and then by line numbers. + // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order + current_diagnostics + .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start)); + } + Entry::Vacant(v) => { + diagnostics.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start)); + v.insert(diagnostics); + } + }; + }; + + let handle_document_diagnostic_report_kind = |editor: &mut Editor, + report: Option< + HashMap, + >| { + for (url, report) in report.into_iter().flatten() { + match report { + lsp::DocumentDiagnosticReportKind::Full(report) => { + let path = url.to_file_path().unwrap(); + parse_diagnostic(editor, path, report.items, report.result_id); + } + lsp::DocumentDiagnosticReportKind::Unchanged(report) => { + let Some(doc) = editor.document_by_path_mut(url.path()) else { + return; + }; + doc.previous_diagnostic_id = Some(report.result_id); + } + } + } + }; + + if let Some(response) = response { + match response { + lsp::DocumentDiagnosticReport::Full(report) => { + // Original file diagnostic + parse_diagnostic( + editor, + original_path, + report.full_document_diagnostic_report.items, + report.full_document_diagnostic_report.result_id, + ); + + // Related files diagnostic + handle_document_diagnostic_report_kind(editor, report.related_documents); + } + lsp::DocumentDiagnosticReport::Unchanged(report) => { + doc.previous_diagnostic_id = + Some(report.unchanged_document_diagnostic_report.result_id); + handle_document_diagnostic_report_kind(editor, report.related_documents); + } + } + } + }, + ); + + jobs.callback(callback); +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index c151a7dd5..e54c8a543 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1066,6 +1066,7 @@ impl EditorView { pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult { commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs); + commands::pull_diagnostic_for_current_doc(cx.editor, cx.jobs); EventResult::Ignored(None) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f3ace89e5..106aae599 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -187,6 +187,8 @@ pub struct Document { pub focused_at: std::time::Instant, pub readonly: bool, + + pub previous_diagnostic_id: Option, } /// Inlay hints for a single `(Document, View)` combo. @@ -677,6 +679,7 @@ impl Document { focused_at: std::time::Instant::now(), readonly: false, jump_labels: HashMap::new(), + previous_diagnostic_id: None, } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cead30d7c..d0e24d5f3 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -25,7 +25,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ borrow::Cow, cell::Cell, - collections::{BTreeMap, HashMap, HashSet}, + collections::{btree_map::Entry, BTreeMap, HashMap, HashSet}, fs, io::{self, stdin}, num::NonZeroUsize, @@ -1934,6 +1934,31 @@ impl Editor { .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false)) } + pub fn add_diagnostics( + &mut self, + diagnostics: Vec, + path: lsp::Url, + server_id: LanguageServerId, + ) { + let mut diagnostics = diagnostics.into_iter().map(|d| (d, server_id)).collect(); + let uri = helix_core::Uri::try_from(path).unwrap(); + match self.diagnostics.entry(uri) { + Entry::Occupied(o) => { + let current_diagnostics = o.into_mut(); + // there may entries of other language servers, which is why we can't overwrite the whole entry + current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id); + current_diagnostics.append(&mut diagnostics); + // Sort diagnostics first by severity and then by line numbers. + // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order + current_diagnostics.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start)); + } + Entry::Vacant(v) => { + diagnostics.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start)); + v.insert(diagnostics); + } + }; + } + /// Returns all supported diagnostics for the document pub fn doc_diagnostics<'a>( language_servers: &'a helix_lsp::Registry, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7c7cb7f41..266aa210e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "1.74.0" -components = ["rustfmt", "rust-src", "clippy"] +components = ["rustfmt", "rust-src", "clippy", "rust-analyzer"]