Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/handlers/diagnostics.rs')
-rw-r--r--helix-term/src/handlers/diagnostics.rs283
1 files changed, 280 insertions, 3 deletions
diff --git a/helix-term/src/handlers/diagnostics.rs b/helix-term/src/handlers/diagnostics.rs
index 3e44d416..aa154eb6 100644
--- a/helix-term/src/handlers/diagnostics.rs
+++ b/helix-term/src/handlers/diagnostics.rs
@@ -1,12 +1,28 @@
-use helix_event::{register_hook, send_blocking};
+use futures_util::stream::FuturesUnordered;
+use std::collections::HashSet;
+use std::mem;
+use std::time::Duration;
+use tokio::time::Instant;
+use tokio_stream::StreamExt;
+
+use helix_core::diagnostic::DiagnosticProvider;
+use helix_core::syntax::config::LanguageServerFeature;
+use helix_core::Uri;
+use helix_event::{cancelable_future, register_hook, send_blocking};
+use helix_lsp::{lsp, LanguageServerId};
use helix_view::document::Mode;
-use helix_view::events::DiagnosticsDidChange;
+use helix_view::events::{
+ DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen, LanguageServerInitialized,
+};
use helix_view::handlers::diagnostics::DiagnosticEvent;
+use helix_view::handlers::lsp::{PullAllDocumentsDiagnosticsEvent, PullDiagnosticsEvent};
use helix_view::handlers::Handlers;
+use helix_view::{DocumentId, Editor};
use crate::events::OnModeSwitch;
+use crate::job;
-pub(super) fn register_hooks(_handlers: &Handlers) {
+pub(super) fn register_hooks(handlers: &Handlers) {
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
if event.editor.mode != Mode::Insert {
for (view, _) in event.editor.tree.views_mut() {
@@ -21,4 +37,265 @@ pub(super) fn register_hooks(_handlers: &Handlers) {
}
Ok(())
});
+
+ let tx = handlers.pull_diagnostics.clone();
+ let tx_all_documents = handlers.pull_all_documents_diagnostics.clone();
+ register_hook!(move |event: &mut DocumentDidChange<'_>| {
+ if event
+ .doc
+ .has_language_server_with_feature(LanguageServerFeature::PullDiagnostics)
+ && !event.ghost_transaction
+ {
+ // Cancel the ongoing request, if present.
+ event.doc.pull_diagnostic_controller.cancel();
+ let document_id = event.doc.id();
+ send_blocking(&tx, PullDiagnosticsEvent { document_id });
+
+ let inter_file_dependencies_language_servers = event
+ .doc
+ .language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
+ .filter(|language_server| {
+ language_server
+ .capabilities()
+ .diagnostic_provider
+ .as_ref()
+ .is_some_and(|diagnostic_provider| match diagnostic_provider {
+ lsp::DiagnosticServerCapabilities::Options(options) => {
+ options.inter_file_dependencies
+ }
+
+ lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
+ options.diagnostic_options.inter_file_dependencies
+ }
+ })
+ })
+ .map(|language_server| language_server.id())
+ .collect();
+
+ send_blocking(
+ &tx_all_documents,
+ PullAllDocumentsDiagnosticsEvent {
+ language_servers: inter_file_dependencies_language_servers,
+ },
+ );
+ }
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut DocumentDidOpen<'_>| {
+ request_document_diagnostics(event.editor, event.doc);
+
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
+ let doc_ids: Vec<_> = event.editor.documents.keys().copied().collect();
+
+ for doc_id in doc_ids {
+ request_document_diagnostics(event.editor, doc_id);
+ }
+
+ Ok(())
+ });
+}
+
+#[derive(Debug, Default)]
+pub(super) struct PullDiagnosticsHandler {
+ document_ids: HashSet<DocumentId>,
+}
+
+impl helix_event::AsyncHook for PullDiagnosticsHandler {
+ type Event = PullDiagnosticsEvent;
+
+ fn handle_event(
+ &mut self,
+ event: Self::Event,
+ _timeout: Option<tokio::time::Instant>,
+ ) -> Option<tokio::time::Instant> {
+ self.document_ids.insert(event.document_id);
+ Some(Instant::now() + Duration::from_millis(250))
+ }
+
+ fn finish_debounce(&mut self) {
+ let document_ids = mem::take(&mut self.document_ids);
+ job::dispatch_blocking(move |editor, _| {
+ for document_id in document_ids {
+ request_document_diagnostics(editor, document_id);
+ }
+ })
+ }
+}
+
+#[derive(Debug, Default)]
+pub(super) struct PullAllDocumentsDiagnosticHandler {
+ language_servers: HashSet<LanguageServerId>,
+}
+
+impl helix_event::AsyncHook for PullAllDocumentsDiagnosticHandler {
+ type Event = PullAllDocumentsDiagnosticsEvent;
+
+ fn handle_event(
+ &mut self,
+ event: Self::Event,
+ _timeout: Option<tokio::time::Instant>,
+ ) -> Option<tokio::time::Instant> {
+ self.language_servers.extend(&event.language_servers);
+ Some(Instant::now() + Duration::from_secs(1))
+ }
+
+ fn finish_debounce(&mut self) {
+ let language_servers = mem::take(&mut self.language_servers);
+ job::dispatch_blocking(move |editor, _| {
+ let documents: Vec<_> = editor.documents.keys().copied().collect();
+
+ for document in documents {
+ request_document_diagnostics_for_language_severs(
+ editor,
+ document,
+ language_servers.clone(),
+ );
+ }
+ })
+ }
+}
+
+fn request_document_diagnostics_for_language_severs(
+ editor: &mut Editor,
+ doc_id: DocumentId,
+ language_servers: HashSet<LanguageServerId>,
+) {
+ let Some(doc) = editor.document_mut(doc_id) else {
+ return;
+ };
+
+ let cancel = doc.pull_diagnostic_controller.restart();
+
+ let mut futures: FuturesUnordered<_> = language_servers
+ .iter()
+ .filter_map(|x| doc.language_servers().find(|y| &y.id() == x))
+ .filter_map(|language_server| {
+ let future = language_server
+ .text_document_diagnostic(doc.identifier(), doc.previous_diagnostic_id.clone())?;
+
+ let identifier = language_server
+ .capabilities()
+ .diagnostic_provider
+ .as_ref()
+ .and_then(|diagnostic_provider| match diagnostic_provider {
+ lsp::DiagnosticServerCapabilities::Options(options) => {
+ options.identifier.clone()
+ }
+ lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
+ options.diagnostic_options.identifier.clone()
+ }
+ });
+
+ let language_server_id = language_server.id();
+ let provider = DiagnosticProvider::Lsp {
+ server_id: language_server_id,
+ identifier,
+ };
+ let uri = doc.uri()?;
+
+ Some(async move {
+ let result = future.await;
+
+ (result, provider, uri)
+ })
+ })
+ .collect();
+
+ if futures.is_empty() {
+ return;
+ }
+
+ tokio::spawn(async move {
+ let mut retry_language_servers = HashSet::new();
+ loop {
+ match cancelable_future(futures.next(), &cancel).await {
+ Some(Some((Ok(result), provider, uri))) => {
+ job::dispatch(move |editor, _| {
+ handle_pull_diagnostics_response(editor, result, provider, uri, doc_id);
+ })
+ .await;
+ }
+ Some(Some((Err(err), DiagnosticProvider::Lsp { server_id, .. }, _))) => {
+ let parsed_cancellation_data = if let helix_lsp::Error::Rpc(error) = err {
+ error.data.and_then(|data| {
+ serde_json::from_value::<lsp::DiagnosticServerCancellationData>(data)
+ .ok()
+ })
+ } else {
+ log::error!("Pull diagnostic request failed: {err}");
+ continue;
+ };
+ if parsed_cancellation_data.is_some_and(|data| data.retrigger_request) {
+ retry_language_servers.insert(server_id);
+ }
+ }
+ Some(None) => break,
+ // The request was cancelled.
+ None => return,
+ }
+ }
+
+ if !retry_language_servers.is_empty() {
+ tokio::time::sleep(Duration::from_millis(500)).await;
+
+ job::dispatch(move |editor, _| {
+ request_document_diagnostics_for_language_severs(
+ editor,
+ doc_id,
+ retry_language_servers,
+ );
+ })
+ .await;
+ }
+ });
+}
+
+pub fn request_document_diagnostics(editor: &mut Editor, doc_id: DocumentId) {
+ let Some(doc) = editor.document(doc_id) else {
+ return;
+ };
+
+ let language_servers = doc
+ .language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
+ .map(|language_servers| language_servers.id())
+ .collect();
+
+ request_document_diagnostics_for_language_severs(editor, doc_id, language_servers);
+}
+
+fn handle_pull_diagnostics_response(
+ editor: &mut Editor,
+ result: lsp::DocumentDiagnosticReportResult,
+ provider: DiagnosticProvider,
+ uri: Uri,
+ document_id: DocumentId,
+) {
+ match result {
+ lsp::DocumentDiagnosticReportResult::Report(report) => {
+ let result_id = match report {
+ lsp::DocumentDiagnosticReport::Full(report) => {
+ editor.handle_lsp_diagnostics(
+ &provider,
+ uri,
+ None,
+ report.full_document_diagnostic_report.items,
+ );
+
+ report.full_document_diagnostic_report.result_id
+ }
+ lsp::DocumentDiagnosticReport::Unchanged(report) => {
+ Some(report.unchanged_document_diagnostic_report.result_id)
+ }
+ };
+
+ if let Some(doc) = editor.document_mut(document_id) {
+ doc.previous_diagnostic_id = result_id;
+ };
+ }
+ lsp::DocumentDiagnosticReportResult::Partial(_) => {}
+ };
}