Support pull diagnostics
This commit is contained in:
parent
dbaa636683
commit
58447dae60
7 changed files with 209 additions and 4 deletions
|
@ -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…
Add table
Reference in a new issue