Support pull diagnostics

pull/11315/head
Sofus Addington 4 months ago
parent dbaa636683
commit 58447dae60

@ -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",
};

@ -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<Notify>,
/// 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<lsp::Url>,
@ -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<String>,
) -> Option<impl Future<Output = Result<Value>>> {
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::<lsp::request::DocumentDiagnosticRequest>(params))
}
pub fn text_document_document_highlight(
&self,
text_document: lsp::TextDocumentIdentifier,

@ -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<lsp::DocumentDiagnosticReport>| {
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<lsp::Diagnostic>,
result_id: Option<String>| {
if let Some(doc) = editor.document_by_path_mut(&path) {
let diagnostics: Vec<helix_core::Diagnostic> = 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<lsp::Url, lsp::DocumentDiagnosticReportKind>,
>| {
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);
}

@ -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)
}

@ -187,6 +187,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.
@ -677,6 +679,7 @@ impl Document {
focused_at: std::time::Instant::now(),
readonly: false,
jump_labels: HashMap::new(),
previous_diagnostic_id: None,
}
}

@ -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<lsp::Diagnostic>,
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,

@ -1,3 +1,3 @@
[toolchain]
channel = "1.74.0"
components = ["rustfmt", "rust-src", "clippy"]
components = ["rustfmt", "rust-src", "clippy", "rust-analyzer"]

Loading…
Cancel
Save