Pull diagnostics

This commit is contained in:
Sofus Addington 2025-01-27 08:22:21 +01:00
parent 35575b0b0f
commit d5c6f805cf
No known key found for this signature in database
GPG key ID: F95B6BA66FD79AFA
13 changed files with 295 additions and 6 deletions

View file

@ -332,6 +332,7 @@ pub enum LanguageServerFeature {
WorkspaceSymbols, WorkspaceSymbols,
// Symbols, use bitflags, see above? // Symbols, use bitflags, see above?
Diagnostics, Diagnostics,
PullDiagnostics,
RenameSymbol, RenameSymbol,
InlayHints, InlayHints,
} }
@ -355,6 +356,7 @@ impl Display for LanguageServerFeature {
DocumentSymbols => "document-symbols", DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols", WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics", Diagnostics => "diagnostics",
PullDiagnostics => "pull-diagnostics",
RenameSymbol => "rename-symbol", RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints", InlayHints => "inlay-hints",
}; };

View file

@ -348,6 +348,7 @@ impl Client {
Some(OneOf::Left(true) | OneOf::Right(_)) Some(OneOf::Left(true) | OneOf::Right(_))
), ),
LanguageServerFeature::Diagnostics => true, // there's no extra server capability LanguageServerFeature::Diagnostics => true, // there's no extra server capability
LanguageServerFeature::PullDiagnostics => capabilities.diagnostic_provider.is_some(),
LanguageServerFeature::RenameSymbol => matches!( LanguageServerFeature::RenameSymbol => matches!(
capabilities.rename_provider, capabilities.rename_provider,
Some(OneOf::Left(true)) | Some(OneOf::Right(_)) Some(OneOf::Left(true)) | Some(OneOf::Right(_))
@ -581,6 +582,9 @@ impl Client {
did_rename: Some(true), did_rename: Some(true),
..Default::default() ..Default::default()
}), }),
diagnostic: Some(lsp::DiagnosticWorkspaceClientCapabilities {
refresh_support: Some(true),
}),
..Default::default() ..Default::default()
}), }),
text_document: Some(lsp::TextDocumentClientCapabilities { text_document: Some(lsp::TextDocumentClientCapabilities {
@ -658,6 +662,10 @@ impl Client {
}), }),
..Default::default() ..Default::default()
}), }),
diagnostic: Some(lsp::DiagnosticClientCapabilities {
dynamic_registration: Some(false),
related_document_support: Some(true),
}),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities { publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
version_support: Some(true), version_support: Some(true),
tag_support: Some(lsp::TagSupport { tag_support: Some(lsp::TagSupport {
@ -1223,6 +1231,32 @@ impl Client {
}) })
} }
pub fn text_document_diagnostic(
&self,
text_document: lsp::TextDocumentIdentifier,
previous_result_id: Option<String>,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities();
// 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::<lsp::request::DocumentDiagnosticRequest>(params))
}
pub fn text_document_document_highlight( pub fn text_document_document_highlight(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,

View file

@ -463,6 +463,7 @@ pub enum MethodCall {
RegisterCapability(lsp::RegistrationParams), RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams), UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams), ShowDocument(lsp::ShowDocumentParams),
WorkspaceDiagnosticRefresh,
} }
impl MethodCall { impl MethodCall {
@ -494,6 +495,7 @@ impl MethodCall {
let params: lsp::ShowDocumentParams = params.parse()?; let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params) Self::ShowDocument(params)
} }
lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh,
_ => { _ => {
return Err(Error::Unhandled); return Err(Error::Unhandled);
} }

View file

@ -733,6 +733,15 @@ impl Application {
doc.text(), doc.text(),
language_id, language_id,
); );
if language_server
.supports_feature(syntax::LanguageServerFeature::PullDiagnostics)
{
handlers::diagnostics::pull_diagnostics_for_document(
doc,
language_server,
);
}
} }
} }
Notification::PublishDiagnostics(params) => { Notification::PublishDiagnostics(params) => {
@ -753,6 +762,7 @@ impl Application {
uri, uri,
params.version, params.version,
params.diagnostics, params.diagnostics,
None,
); );
} }
Notification::ShowMessage(params) => { Notification::ShowMessage(params) => {
@ -1028,6 +1038,16 @@ impl Application {
let result = self.handle_show_document(params, offset_encoding); let result = self.handle_show_document(params, offset_encoding);
Ok(json!(result)) Ok(json!(result))
} }
Ok(MethodCall::WorkspaceDiagnosticRefresh) => {
for document in self.editor.documents() {
let language_server = language_server!();
handlers::diagnostics::pull_diagnostics_for_document(
document,
language_server,
);
}
Ok(serde_json::Value::Null)
}
}; };
let language_server = language_server!(); let language_server = language_server!();

View file

@ -916,6 +916,10 @@ fn goto_buffer(editor: &mut Editor, direction: Direction, count: usize) {
let id = *id; let id = *id;
if let Some(doc) = editor.document(id) {
helix_event::dispatch(helix_view::events::DocumentDidOpen { doc });
};
editor.switch(id, Action::Replace); editor.switch(id, Action::Replace);
} }

View file

@ -1,7 +1,7 @@
use helix_event::{events, register_event}; use helix_event::{events, register_event};
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::events::{ use helix_view::events::{
DiagnosticsDidChange, DocumentDidChange, DocumentFocusLost, SelectionDidChange, DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen, DocumentFocusLost, SelectionDidChange,
}; };
use crate::commands; use crate::commands;
@ -18,6 +18,7 @@ pub fn register() {
register_event::<PostInsertChar>(); register_event::<PostInsertChar>();
register_event::<PostCommand>(); register_event::<PostCommand>();
register_event::<DocumentDidChange>(); register_event::<DocumentDidChange>();
register_event::<DocumentDidOpen>();
register_event::<DocumentFocusLost>(); register_event::<DocumentFocusLost>();
register_event::<SelectionDidChange>(); register_event::<SelectionDidChange>();
register_event::<DiagnosticsDidChange>(); register_event::<DiagnosticsDidChange>();

View file

@ -6,13 +6,14 @@ use helix_event::AsyncHook;
use crate::config::Config; use crate::config::Config;
use crate::events; use crate::events;
use crate::handlers::auto_save::AutoSaveHandler; use crate::handlers::auto_save::AutoSaveHandler;
use crate::handlers::diagnostics::PullDiagnosticsHandler;
use crate::handlers::signature_help::SignatureHelpHandler; use crate::handlers::signature_help::SignatureHelpHandler;
pub use helix_view::handlers::Handlers; pub use helix_view::handlers::Handlers;
mod auto_save; mod auto_save;
pub mod completion; pub mod completion;
mod diagnostics; pub mod diagnostics;
mod signature_help; mod signature_help;
mod snippet; mod snippet;
@ -22,11 +23,13 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let event_tx = completion::CompletionHandler::new(config).spawn(); let event_tx = completion::CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn(); let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn(); let auto_save = AutoSaveHandler::new().spawn();
let pull_diagnostics = PullDiagnosticsHandler::new().spawn();
let handlers = Handlers { let handlers = Handlers {
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx), completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
signature_hints, signature_hints,
auto_save, auto_save,
pull_diagnostics,
}; };
completion::register_hooks(&handlers); completion::register_hooks(&handlers);

View file

@ -1,12 +1,22 @@
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use helix_core::syntax::LanguageServerFeature;
use helix_core::Uri;
use helix_event::{register_hook, send_blocking}; use helix_event::{register_hook, send_blocking};
use helix_lsp::{lsp, LanguageServerId};
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::events::DiagnosticsDidChange; use helix_view::events::{DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen};
use helix_view::handlers::diagnostics::DiagnosticEvent; use helix_view::handlers::diagnostics::DiagnosticEvent;
use helix_view::handlers::lsp::PullDiagnosticsEvent;
use helix_view::handlers::Handlers; use helix_view::handlers::Handlers;
use helix_view::{DocumentId, Editor};
use tokio::time::Instant;
use crate::events::OnModeSwitch; 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<'_>| { register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
if event.editor.mode != Mode::Insert { if event.editor.mode != Mode::Insert {
for (view, _) in event.editor.tree.views_mut() { for (view, _) in event.editor.tree.views_mut() {
@ -21,4 +31,194 @@ pub(super) fn register_hooks(_handlers: &Handlers) {
} }
Ok(()) Ok(())
}); });
let tx = handlers.pull_diagnostics.clone();
register_hook!(move |event: &mut DocumentDidChange<'_>| {
if event
.doc
.has_language_server_with_feature(LanguageServerFeature::PullDiagnostics)
{
let document_id = event.doc.id();
send_blocking(&tx, PullDiagnosticsEvent { document_id });
}
Ok(())
});
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
if event
.doc
.has_language_server_with_feature(LanguageServerFeature::PullDiagnostics)
{
let document_id = event.doc.id();
job::dispatch_blocking(move |editor, _| {
let Some(doc) = editor.document(document_id) else {
return;
};
let language_servers =
doc.language_servers_with_feature(LanguageServerFeature::PullDiagnostics);
for language_server in language_servers {
pull_diagnostics_for_document(doc, language_server);
}
})
}
Ok(())
});
}
#[derive(Debug)]
pub(super) struct PullDiagnosticsHandler {
document_ids: HashSet<DocumentId>,
}
impl PullDiagnosticsHandler {
pub fn new() -> PullDiagnosticsHandler {
PullDiagnosticsHandler {
document_ids: [].into(),
}
}
}
impl helix_event::AsyncHook for PullDiagnosticsHandler {
type Event = PullDiagnosticsEvent;
fn handle_event(
&mut self,
event: Self::Event,
existing_debounce: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
if existing_debounce.is_none() {
dispatch_pull_diagnostic_for_document(event.document_id);
}
self.document_ids.insert(event.document_id);
Some(Instant::now() + Duration::from_millis(500))
}
fn finish_debounce(&mut self) {
for document_id in self.document_ids.clone() {
dispatch_pull_diagnostic_for_document(document_id);
}
}
}
fn dispatch_pull_diagnostic_for_document(document_id: DocumentId) {
job::dispatch_blocking(move |editor, _| {
let Some(doc) = editor.document(document_id) else {
return;
};
let language_servers =
doc.language_servers_with_feature(LanguageServerFeature::PullDiagnostics);
for language_server in language_servers {
pull_diagnostics_for_document(doc, language_server);
}
})
}
pub fn pull_diagnostics_for_document(
doc: &helix_view::Document,
language_server: &helix_lsp::Client,
) {
let Some(future) = language_server
.text_document_diagnostic(doc.identifier(), doc.previous_diagnostic_id.clone())
else {
return;
};
let Some(uri) = doc.uri() else {
return;
};
let server_id = language_server.id();
let document_id = doc.id();
tokio::spawn(async move {
match future.await {
Ok(res) => {
job::dispatch(move |editor, _| {
let response = match serde_json::from_value(res) {
Ok(result) => result,
Err(_) => return,
};
handle_pull_diagnostics_response(editor, response, server_id, uri, document_id)
})
.await
}
Err(err) => log::error!("Pull diagnostic request failed: {err}"),
}
});
}
fn handle_pull_diagnostics_response(
editor: &mut Editor,
response: lsp::DocumentDiagnosticReport,
server_id: LanguageServerId,
uri: Uri,
document_id: DocumentId,
) {
let Some(doc) = editor.document_mut(document_id) else {
return;
};
match response {
lsp::DocumentDiagnosticReport::Full(report) => {
// Diagnostic for requested file
editor.handle_lsp_diagnostics(
server_id,
uri,
None,
report.full_document_diagnostic_report.items,
report.full_document_diagnostic_report.result_id,
);
// Diagnostic for related files
handle_document_diagnostic_report_kind(
editor,
document_id,
report.related_documents,
server_id,
);
}
lsp::DocumentDiagnosticReport::Unchanged(report) => {
doc.previous_diagnostic_id =
Some(report.unchanged_document_diagnostic_report.result_id);
handle_document_diagnostic_report_kind(
editor,
document_id,
report.related_documents,
server_id,
);
}
}
}
fn handle_document_diagnostic_report_kind(
editor: &mut Editor,
document_id: DocumentId,
report: Option<HashMap<lsp::Url, lsp::DocumentDiagnosticReportKind>>,
server_id: LanguageServerId,
) {
for (url, report) in report.into_iter().flatten() {
match report {
lsp::DocumentDiagnosticReportKind::Full(report) => {
let Ok(uri) = Uri::try_from(url) else {
return;
};
editor.handle_lsp_diagnostics(server_id, uri, None, report.items, report.result_id);
}
lsp::DocumentDiagnosticReportKind::Unchanged(report) => {
let Some(doc) = editor.document_mut(document_id) else {
return;
};
doc.previous_diagnostic_id = Some(report.result_id);
}
}
}
} }

View file

@ -196,6 +196,8 @@ pub struct Document {
pub focused_at: std::time::Instant, pub focused_at: std::time::Instant,
pub readonly: bool, pub readonly: bool,
pub previous_diagnostic_id: Option<String>,
} }
/// Inlay hints for a single `(Document, View)` combo. /// Inlay hints for a single `(Document, View)` combo.
@ -698,6 +700,7 @@ impl Document {
focused_at: std::time::Instant::now(), focused_at: std::time::Instant::now(),
readonly: false, readonly: false,
jump_labels: HashMap::new(), jump_labels: HashMap::new(),
previous_diagnostic_id: None,
} }
} }
@ -2174,6 +2177,10 @@ impl Document {
pub fn reset_all_inlay_hints(&mut self) { pub fn reset_all_inlay_hints(&mut self) {
self.inlay_hints = Default::default(); self.inlay_hints = Default::default();
} }
pub fn has_language_server_with_feature(&self, feature: LanguageServerFeature) -> bool {
self.language_servers_with_feature(feature).next().is_some()
}
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]

View file

@ -4,7 +4,7 @@ use crate::{
document::{ document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
}, },
events::DocumentFocusLost, events::{DocumentDidOpen, DocumentFocusLost},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
handlers::Handlers, handlers::Handlers,
info::Info, info::Info,
@ -1770,6 +1770,11 @@ impl Editor {
}; };
self.switch(id, action); self.switch(id, action);
if let Some(doc) = self.document_mut(id) {
helix_event::dispatch(DocumentDidOpen { doc });
};
Ok(id) Ok(id)
} }

View file

@ -11,6 +11,7 @@ events! {
changes: &'a ChangeSet, changes: &'a ChangeSet,
ghost_transaction: bool ghost_transaction: bool
} }
DocumentDidOpen<'a> { doc: &'a Document}
SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId } SelectionDidChange<'a> { doc: &'a mut Document, view: ViewId }
DiagnosticsDidChange<'a> { editor: &'a mut Editor, doc: DocumentId } DiagnosticsDidChange<'a> { editor: &'a mut Editor, doc: DocumentId }
// called **after** a document loses focus (but not when its closed) // called **after** a document loses focus (but not when its closed)

View file

@ -21,6 +21,7 @@ pub struct Handlers {
pub completions: CompletionHandler, pub completions: CompletionHandler,
pub signature_hints: Sender<lsp::SignatureHelpEvent>, pub signature_hints: Sender<lsp::SignatureHelpEvent>,
pub auto_save: Sender<AutoSaveEvent>, pub auto_save: Sender<AutoSaveEvent>,
pub pull_diagnostics: Sender<lsp::PullDiagnosticsEvent>,
} }
impl Handlers { impl Handlers {

View file

@ -3,7 +3,7 @@ use std::fmt::Display;
use crate::editor::Action; use crate::editor::Action;
use crate::events::DiagnosticsDidChange; use crate::events::DiagnosticsDidChange;
use crate::Editor; use crate::{DocumentId, Editor};
use helix_core::Uri; use helix_core::Uri;
use helix_lsp::util::generate_transaction_from_edits; use helix_lsp::util::generate_transaction_from_edits;
use helix_lsp::{lsp, LanguageServerId, OffsetEncoding}; use helix_lsp::{lsp, LanguageServerId, OffsetEncoding};
@ -22,6 +22,10 @@ pub enum SignatureHelpEvent {
RequestComplete { open: bool }, RequestComplete { open: bool },
} }
pub struct PullDiagnosticsEvent {
pub document_id: DocumentId,
}
#[derive(Debug)] #[derive(Debug)]
pub struct ApplyEditError { pub struct ApplyEditError {
pub kind: ApplyEditErrorKind, pub kind: ApplyEditErrorKind,
@ -280,6 +284,7 @@ impl Editor {
uri: Uri, uri: Uri,
version: Option<i32>, version: Option<i32>,
mut diagnostics: Vec<lsp::Diagnostic>, mut diagnostics: Vec<lsp::Diagnostic>,
report_id: Option<String>,
) { ) {
let doc = self let doc = self
.documents .documents
@ -357,6 +362,10 @@ impl Editor {
); );
doc.replace_diagnostics(diagnostics, &unchanged_diag_sources, Some(server_id)); doc.replace_diagnostics(diagnostics, &unchanged_diag_sources, Some(server_id));
if report_id.is_some() {
doc.previous_diagnostic_id = report_id;
}
let doc = doc.id(); let doc = doc.id();
helix_event::dispatch(DiagnosticsDidChange { editor: self, doc }); helix_event::dispatch(DiagnosticsDidChange { editor: self, doc });
} }