Merge remote-tracking branch 'helix-pull/pull-diagnostics'

This commit is contained in:
Kalle Carlbark 2025-03-20 09:26:21 +01:00
commit 932158e6a6
17 changed files with 604 additions and 67 deletions

View file

@ -50,8 +50,81 @@ pub struct Diagnostic {
pub data: Option<serde_json::Value>,
}
// TODO turn this into an enum + feature flag when lsp becomes optional
pub type DiagnosticProvider = LanguageServerId;
// TODO turn this into a feature flag when lsp becomes optional
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum DiagnosticProvider {
Lsp {
server_id: LanguageServerId,
identifier: Option<String>,
},
// In the future, other non-LSP providers like spell checking go here...
}
impl DiagnosticProvider {
pub fn from_server_id(server_id: LanguageServerId) -> DiagnosticProvider {
DiagnosticProvider::Lsp {
server_id,
identifier: None,
}
}
pub fn from_server_and_identifier(
server_id: LanguageServerId,
identifier: Option<String>,
) -> DiagnosticProvider {
DiagnosticProvider::Lsp {
server_id,
identifier,
}
}
pub fn server_id(&self) -> &LanguageServerId {
match self {
DiagnosticProvider::Lsp {
server_id,
identifier: _,
} => server_id,
}
}
pub fn has_server_id(&self, server_id: &LanguageServerId) -> bool {
match self {
DiagnosticProvider::Lsp {
server_id: id,
identifier: _,
} => server_id == id,
}
}
pub fn equals(&self, diagnostic_provider: &DiagnosticProvider) -> bool {
let (other_identifier, other_server_id) = match diagnostic_provider {
DiagnosticProvider::Lsp {
server_id,
identifier,
} => (identifier, server_id),
};
let (identifier, server_id) = match self {
DiagnosticProvider::Lsp {
server_id,
identifier,
} => (identifier, server_id),
};
identifier == other_identifier && server_id == other_server_id
}
}
impl From<DiagnosticProvider> for LanguageServerId {
fn from(value: DiagnosticProvider) -> Self {
match value {
DiagnosticProvider::Lsp {
server_id,
identifier: _,
} => server_id,
}
}
}
// while I would prefer having this in helix-lsp that necessitates a bunch of
// conversions I would rather not add. I think its fine since this just a very
@ -73,3 +146,85 @@ impl Diagnostic {
self.severity.unwrap_or(Severity::Warning)
}
}
#[cfg(test)]
mod tests {
use slotmap::KeyData;
use super::DiagnosticProvider;
use crate::diagnostic::LanguageServerId;
#[test]
fn can_compare_equal_diagnostic_provider() {
let first_provider =
DiagnosticProvider::from_server_id(LanguageServerId(KeyData::from_ffi(1)));
let second_provider =
DiagnosticProvider::from_server_id(LanguageServerId(KeyData::from_ffi(1)));
assert!(first_provider.equals(&second_provider));
}
#[test]
fn can_compare_equal_diagnostic_provider_with_identifier() {
let first_provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(1)),
Some("provider".to_string()),
);
let second_provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(1)),
Some("provider".to_string()),
);
assert!(first_provider.equals(&second_provider));
}
#[test]
fn can_distinguish_diagnostic_provider() {
let first_provider =
DiagnosticProvider::from_server_id(LanguageServerId(KeyData::from_ffi(1)));
let second_provider =
DiagnosticProvider::from_server_id(LanguageServerId(KeyData::from_ffi(2)));
assert!(!first_provider.equals(&second_provider));
}
#[test]
fn can_distinguish_diagnostic_provider_by_identifier() {
let first_provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(1)),
Some("provider".to_string()),
);
let second_provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(1)),
None,
);
assert!(!first_provider.equals(&second_provider));
}
#[test]
fn can_distinguish_diagnostic_provider_by_language_server_id() {
let first_provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(1)),
Some("provider".to_string()),
);
let second_provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(2)),
Some("provider".to_string()),
);
assert!(!first_provider.equals(&second_provider));
}
#[test]
fn can_compare_language_server_id() {
let provider = DiagnosticProvider::from_server_and_identifier(
LanguageServerId(KeyData::from_ffi(1)),
Some("provider".to_string()),
);
let language_server_id = LanguageServerId(KeyData::from_ffi(1));
assert!(provider.has_server_id(&language_server_id));
}
}

View file

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

View file

@ -348,6 +348,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(_))
@ -581,6 +582,9 @@ impl Client {
did_rename: Some(true),
..Default::default()
}),
diagnostic: Some(lsp::DiagnosticWorkspaceClientCapabilities {
refresh_support: Some(true),
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -658,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 +1232,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(
&self,
text_document: lsp::TextDocumentIdentifier,

View file

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

View file

@ -1,6 +1,9 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Range, Selection};
use helix_core::{
diagnostic::{DiagnosticProvider, Severity},
pos_at_coords, syntax, Range, Selection,
};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_range_to_range,
@ -725,6 +728,15 @@ impl Application {
doc.text(),
language_id,
);
if language_server
.supports_feature(syntax::LanguageServerFeature::PullDiagnostics)
{
handlers::diagnostics::pull_diagnostics_for_document(
doc,
language_server,
);
}
}
}
Notification::PublishDiagnostics(params) => {
@ -740,8 +752,12 @@ impl Application {
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
return;
}
let diagnostic_provider =
DiagnosticProvider::from_server_id(language_server.id());
self.editor.handle_lsp_diagnostics(
language_server.id(),
&diagnostic_provider,
uri,
params.version,
params.diagnostics,
@ -854,14 +870,16 @@ impl Application {
// we need to clear those and remove the entries from the list if this leads to
// an empty diagnostic list for said files
for diags in self.editor.diagnostics.values_mut() {
diags.retain(|(_, lsp_id)| *lsp_id != server_id);
diags.retain(|(_, diagnostic_provider)| {
!diagnostic_provider.has_server_id(&server_id)
});
}
self.editor.diagnostics.retain(|_, diags| !diags.is_empty());
// Clear any diagnostics for documents with this server open.
for doc in self.editor.documents_mut() {
doc.clear_diagnostics(Some(server_id));
doc.clear_all_language_server_diagnostics(Some(server_id));
}
// Remove the language server from the registry.
@ -1020,6 +1038,16 @@ impl Application {
let result = self.handle_show_document(params, offset_encoding);
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!();

View file

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

View file

@ -14,7 +14,8 @@ use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{
syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri,
diagnostic::DiagnosticProvider, syntax::LanguageServerFeature,
text_annotations::InlineAnnotation, Selection, Uri,
};
use helix_stdx::path;
use helix_view::{
@ -209,7 +210,7 @@ type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>,
format: DiagnosticsFormat,
) -> DiagnosticsPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
@ -219,8 +220,8 @@ fn diag_picker(
for (uri, diags) in diagnostics {
flat_diag.reserve(diags.len());
for (diag, ls) in diags {
if let Some(ls) = cx.editor.language_server_by_id(ls) {
for (diag, provider) in diags {
if let Some(ls) = cx.editor.language_server_by_id(provider.into()) {
flat_diag.push(PickerDiagnostic {
location: Location {
uri: uri.clone(),

View file

@ -1622,7 +1622,7 @@ fn lsp_stop(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> any
for doc in cx.editor.documents_mut() {
if let Some(client) = doc.remove_language_server_by_name(ls_name) {
doc.clear_diagnostics(Some(client.id()));
doc.clear_all_language_server_diagnostics(Some(client.id()));
doc.reset_all_inlay_hints();
doc.inlay_hints_oudated = true;
}

View file

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

View file

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

View file

@ -1,12 +1,22 @@
use std::time::Duration;
use helix_core::diagnostic::DiagnosticProvider;
use helix_core::syntax::LanguageServerFeature;
use helix_core::Uri;
use helix_event::{register_hook, send_blocking};
use helix_lsp::lsp;
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::lsp::PullDiagnosticsEvent;
use helix_view::handlers::Handlers;
use helix_view::{DocumentId, Editor};
use tokio::time::Instant;
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 +31,247 @@ pub(super) fn register_hooks(_handlers: &Handlers) {
}
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<'_>| {
for language_server in event
.doc
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
{
pull_diagnostics_for_document(event.doc, language_server);
}
Ok(())
});
}
#[derive(Debug)]
pub(super) struct PullDiagnosticsHandler {
no_inter_file_dependency_timeout: Option<tokio::time::Instant>,
}
impl PullDiagnosticsHandler {
pub fn new() -> PullDiagnosticsHandler {
PullDiagnosticsHandler {
no_inter_file_dependency_timeout: None,
}
}
}
const TIMEOUT: Duration = Duration::from_millis(500);
const TIMEOUT_NO_INTER_FILE_DEPENDENCY: Duration = Duration::from_millis(125);
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> {
if timeout.is_none() {
dispatch_pull_diagnostic_for_document(event.document_id, false);
self.no_inter_file_dependency_timeout = Some(Instant::now());
}
if self
.no_inter_file_dependency_timeout
.is_some_and(|nifd_timeout| {
nifd_timeout.duration_since(Instant::now()) > TIMEOUT_NO_INTER_FILE_DEPENDENCY
})
{
dispatch_pull_diagnostic_for_document(event.document_id, true);
self.no_inter_file_dependency_timeout = Some(Instant::now());
};
Some(Instant::now() + TIMEOUT)
}
fn finish_debounce(&mut self) {
dispatch_pull_diagnostic_for_open_documents();
}
}
fn dispatch_pull_diagnostic_for_document(
document_id: DocumentId,
exclude_language_servers_without_inter_file_dependency: bool,
) {
job::dispatch_blocking(move |editor, _| {
let Some(doc) = editor.document(document_id) else {
return;
};
let language_servers = doc
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
.filter(|ls| ls.is_initialized())
.filter(|ls| {
if !exclude_language_servers_without_inter_file_dependency {
return true;
};
ls.capabilities()
.diagnostic_provider
.as_ref()
.is_some_and(|dp| match dp {
lsp::DiagnosticServerCapabilities::Options(options) => {
options.inter_file_dependencies
}
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
options.diagnostic_options.inter_file_dependencies
}
})
});
for language_server in language_servers {
pull_diagnostics_for_document(doc, language_server);
}
})
}
fn dispatch_pull_diagnostic_for_open_documents() {
job::dispatch_blocking(move |editor, _| {
let documents = editor.documents.values();
for document in documents {
let language_servers = document
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
.filter(|ls| ls.is_initialized());
for language_server in language_servers {
pull_diagnostics_for_document(document, 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 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::from_server_and_identifier(language_server_id, identifier);
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, provider, uri, document_id)
})
.await
}
Err(err) => {
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}");
return;
};
if let Some(parsed_cancellation_data) = parsed_cancellation_data {
if parsed_cancellation_data.retrigger_request {
tokio::time::sleep(Duration::from_millis(500)).await;
job::dispatch(move |editor, _| {
if let (Some(doc), Some(language_server)) = (
editor.document(document_id),
editor.language_server_by_id(language_server_id),
) {
pull_diagnostics_for_document(doc, language_server);
}
})
.await;
}
}
}
}
});
}
fn handle_pull_diagnostics_response(
editor: &mut Editor,
response: lsp::DocumentDiagnosticReport,
provider: DiagnosticProvider,
uri: Uri,
document_id: DocumentId,
) {
let (result_id, related_documents) = match response {
lsp::DocumentDiagnosticReport::Full(report) => {
editor.handle_lsp_diagnostics(
&provider,
uri,
None,
report.full_document_diagnostic_report.items,
);
(
report.full_document_diagnostic_report.result_id,
report.related_documents,
)
}
lsp::DocumentDiagnosticReport::Unchanged(report) => (
Some(report.unchanged_document_diagnostic_report.result_id),
report.related_documents,
),
};
if let Some(doc) = editor.document_mut(document_id) {
doc.previous_diagnostic_id = result_id;
};
for (url, report) in related_documents.into_iter().flatten() {
let result_id = match report {
lsp::DocumentDiagnosticReportKind::Full(report) => {
let Ok(uri) = Uri::try_from(url) else {
continue;
};
editor.handle_lsp_diagnostics(&provider, uri, None, report.items);
report.result_id
}
lsp::DocumentDiagnosticReportKind::Unchanged(report) => Some(report.result_id),
};
if let Some(doc) = editor.document_mut(document_id) {
doc.previous_diagnostic_id = result_id;
}
}
}

View file

@ -5,6 +5,7 @@ use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
use helix_core::chars::char_is_word;
use helix_core::diagnostic::DiagnosticProvider;
use helix_core::doc_formatter::TextFormat;
use helix_core::encoding::Encoding;
use helix_core::snippets::{ActiveSnippet, SnippetRenderCtx};
@ -196,6 +197,8 @@ pub struct Document {
pub focused_at: std::time::Instant,
pub readonly: bool,
pub previous_diagnostic_id: Option<String>,
}
/// Inlay hints for a single `(Document, View)` combo.
@ -698,6 +701,7 @@ impl Document {
focused_at: std::time::Instant::now(),
readonly: false,
jump_labels: HashMap::new(),
previous_diagnostic_id: None,
}
}
@ -1403,8 +1407,13 @@ impl Document {
true
});
self.diagnostics
.sort_by_key(|diagnostic| (diagnostic.range, diagnostic.severity, diagnostic.provider));
self.diagnostics.sort_by_key(|diagnostic| {
(
diagnostic.range,
diagnostic.severity,
diagnostic.provider.clone(),
)
});
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
let apply_inlay_hint_changes = |annotations: &mut Vec<InlineAnnotation>| {
@ -1931,7 +1940,7 @@ impl Document {
text: &Rope,
language_config: Option<&LanguageConfiguration>,
diagnostic: &helix_lsp::lsp::Diagnostic,
language_server_id: LanguageServerId,
language_server_id: DiagnosticProvider,
offset_encoding: helix_lsp::OffsetEncoding,
) -> Option<Diagnostic> {
use helix_core::diagnostic::{Range, Severity::*};
@ -2024,13 +2033,16 @@ impl Document {
&mut self,
diagnostics: impl IntoIterator<Item = Diagnostic>,
unchanged_sources: &[String],
language_server_id: Option<LanguageServerId>,
diagnostic_provider: Option<&DiagnosticProvider>,
) {
if unchanged_sources.is_empty() {
self.clear_diagnostics(language_server_id);
self.clear_diagnostics(diagnostic_provider);
} else {
self.diagnostics.retain(|d| {
if language_server_id.is_some_and(|id| id != d.provider) {
if diagnostic_provider
.as_ref()
.is_some_and(|provider| !provider.equals(&d.provider))
{
return true;
}
@ -2042,14 +2054,29 @@ impl Document {
});
}
self.diagnostics.extend(diagnostics);
self.diagnostics
.sort_by_key(|diagnostic| (diagnostic.range, diagnostic.severity, diagnostic.provider));
self.diagnostics.sort_by_key(|diagnostic| {
(
diagnostic.range,
diagnostic.severity,
diagnostic.provider.clone(),
)
});
}
/// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared
pub fn clear_diagnostics(&mut self, language_server_id: Option<LanguageServerId>) {
if let Some(id) = language_server_id {
self.diagnostics.retain(|d| d.provider != id);
/// clears diagnostics for a given diagnostic provider if set, otherwise all diagnostics are cleared
pub fn clear_diagnostics(&mut self, provider: Option<&DiagnosticProvider>) {
if let Some(provider) = provider {
self.diagnostics.retain(|d| !d.provider.equals(provider));
} else {
self.diagnostics.clear();
}
}
/// clears diagnostics for a given language_server if set, otherwise all diagnostics are cleared
pub fn clear_all_language_server_diagnostics(&mut self, server_id: Option<LanguageServerId>) {
if let Some(server_id) = server_id {
self.diagnostics
.retain(|d| !d.provider.has_server_id(&server_id));
} else {
self.diagnostics.clear();
}
@ -2174,6 +2201,10 @@ impl Document {
pub fn reset_all_inlay_hints(&mut self) {
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)]

View file

@ -4,7 +4,7 @@ use crate::{
document::{
DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
},
events::DocumentFocusLost,
events::{DocumentDidOpen, DocumentFocusLost},
graphics::{CursorKind, Rect},
handlers::Handlers,
info::Info,
@ -45,6 +45,7 @@ use anyhow::{anyhow, bail, Error};
pub use helix_core::diagnostic::Severity;
use helix_core::{
auto_pairs::AutoPairs,
diagnostic::DiagnosticProvider,
syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING,
};
@ -1056,7 +1057,7 @@ pub struct Editor {
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
pub diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>,
pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>,
@ -1778,6 +1779,11 @@ impl Editor {
};
self.switch(id, action);
if let Some(doc) = self.document_mut(id) {
helix_event::dispatch(DocumentDidOpen { doc });
};
Ok(id)
}
@ -2001,7 +2007,7 @@ impl Editor {
/// Returns all supported diagnostics for the document
pub fn doc_diagnostics<'a>(
language_servers: &'a helix_lsp::Registry,
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>,
document: &Document,
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
@ -2011,9 +2017,9 @@ impl Editor {
/// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
pub fn doc_diagnostics_with_filter<'a>(
language_servers: &'a helix_lsp::Registry,
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>,
document: &Document,
filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a,
filter: impl Fn(&lsp::Diagnostic, DiagnosticProvider) -> bool + 'a,
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
let text = document.text().clone();
let language_config = document.language.clone();
@ -2021,30 +2027,32 @@ impl Editor {
.uri()
.and_then(|uri| diagnostics.get(&uri))
.map(|diags| {
diags.iter().filter_map(move |(diagnostic, lsp_id)| {
let ls = language_servers.get_by_id(*lsp_id)?;
language_config
.as_ref()
.and_then(|c| {
c.language_servers.iter().find(|features| {
features.name == ls.name()
&& features.has_feature(LanguageServerFeature::Diagnostics)
diags
.iter()
.filter_map(move |(diagnostic, diagnostic_provider)| {
let ls = language_servers.get_by_id(*diagnostic_provider.server_id())?;
language_config
.as_ref()
.and_then(|c| {
c.language_servers.iter().find(|features| {
features.name == ls.name()
&& features.has_feature(LanguageServerFeature::Diagnostics)
})
})
})
.and_then(|_| {
if filter(diagnostic, *lsp_id) {
Document::lsp_diagnostic_to_diagnostic(
&text,
language_config.as_deref(),
diagnostic,
*lsp_id,
ls.offset_encoding(),
)
} else {
None
}
})
})
.and_then(|_| {
if filter(diagnostic, diagnostic_provider.clone()) {
Document::lsp_diagnostic_to_diagnostic(
&text,
language_config.as_deref(),
diagnostic,
diagnostic_provider.clone(),
ls.offset_encoding(),
)
} else {
None
}
})
})
})
.into_iter()
.flatten()

View file

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

View file

@ -71,7 +71,7 @@ pub fn diagnostic<'doc>(
d.line == line
&& doc
.language_servers_with_feature(LanguageServerFeature::Diagnostics)
.any(|ls| ls.id() == d.provider)
.any(|ls| d.provider.has_server_id(&ls.id()))
});
diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
write!(out, "").ok();

View file

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

View file

@ -3,10 +3,11 @@ use std::fmt::Display;
use crate::editor::Action;
use crate::events::DiagnosticsDidChange;
use crate::Editor;
use crate::{DocumentId, Editor};
use helix_core::diagnostic::DiagnosticProvider;
use helix_core::Uri;
use helix_lsp::util::generate_transaction_from_edits;
use helix_lsp::{lsp, LanguageServerId, OffsetEncoding};
use helix_lsp::{lsp, OffsetEncoding};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
@ -22,6 +23,10 @@ pub enum SignatureHelpEvent {
RequestComplete { open: bool },
}
pub struct PullDiagnosticsEvent {
pub document_id: DocumentId,
}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
@ -276,7 +281,7 @@ impl Editor {
pub fn handle_lsp_diagnostics(
&mut self,
server_id: LanguageServerId,
diagnostic_provider: &DiagnosticProvider,
uri: Uri,
version: Option<i32>,
mut diagnostics: Vec<lsp::Diagnostic>,
@ -309,8 +314,8 @@ impl Editor {
.filter(|d| d.source.as_ref() == Some(source));
let old_diagnostics = old_diagnostics
.iter()
.filter(|(d, d_server)| {
*d_server == server_id && d.source.as_ref() == Some(source)
.filter(|(d, d_provider)| {
diagnostic_provider.equals(d_provider) && d.source.as_ref() == Some(source)
})
.map(|(d, _)| d);
if new_diagnostics.eq(old_diagnostics) {
@ -319,7 +324,9 @@ impl Editor {
}
}
let diagnostics = diagnostics.into_iter().map(|d| (d, server_id));
let diagnostics = diagnostics
.into_iter()
.map(|d| (d, diagnostic_provider.clone()));
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnostic message and so we can't calculate the exact position.
@ -328,7 +335,8 @@ impl Editor {
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
.retain(|(_, ls_provider)| !diagnostic_provider.equals(ls_provider));
current_diagnostics.extend(diagnostics);
current_diagnostics
// Sort diagnostics first by severity and then by line numbers.
@ -338,12 +346,13 @@ impl Editor {
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
diagnostics.sort_by_key(|(d, server_id)| (d.severity, d.range.start, *server_id));
diagnostics
.sort_by_key(|(d, ls_provider)| (d.severity, d.range.start, ls_provider.clone()));
if let Some(doc) = doc {
let diagnostic_of_language_server_and_not_in_unchanged_sources =
|diagnostic: &lsp::Diagnostic, ls_id| {
ls_id == server_id
|diagnostic: &lsp::Diagnostic, ls_provider| {
diagnostic_provider.equals(&ls_provider)
&& diagnostic
.source
.as_ref()
@ -355,7 +364,11 @@ impl Editor {
doc,
diagnostic_of_language_server_and_not_in_unchanged_sources,
);
doc.replace_diagnostics(diagnostics, &unchanged_diag_sources, Some(server_id));
doc.replace_diagnostics(
diagnostics,
&unchanged_diag_sources,
Some(diagnostic_provider),
);
let doc = doc.id();
helix_event::dispatch(DiagnosticsDidChange { editor: self, doc });