lsp: Implement support for the textDocument/diagnostic
command (#19230)
Closes [#13107](https://github.com/zed-industries/zed/issues/13107) Enabled pull diagnostics by default, for the language servers that declare support in the corresponding capabilities. ``` "diagnostics": { "lsp_pull_diagnostics_debounce_ms": null } ``` settings can be used to disable the pulling. Release Notes: - Added support for the LSP `textDocument/diagnostic` command. # Brief This is draft PR that implements the LSP `textDocument/diagnostic` command. The goal is to receive your feedback and establish further steps towards fully implementing this command. I tried to re-use existing method and structures to ensure: 1. The existing functionality works as before 2. There is no interference between the diagnostics sent by a server and the diagnostics requested by a client. The current implementation is done via a new LSP command `GetDocumentDiagnostics` that is sent when a buffer is saved and when a buffer is edited. There is a new method called `pull_diagnostic` that is called for such events. It has debounce to ensure we don't spam a server with commands every time the buffer is edited. Probably, we don't need the debounce when the buffer is saved. All in all, the goal is basically to get your feedback and ensure I am on the right track. Thanks! ## References 1. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics ## In action You can clone any Ruby repo since the `ruby-lsp` supports the pull diagnostics only. Steps to reproduce: 1. Clone this repo https://github.com/vitallium/stimulus-lsp-error-zed 2. Install Ruby (via `asdf` or `mise). 4. Install Ruby gems via `bundle install` 5. Install Ruby LSP with `gem install ruby-lsp` 6. Check out this PR and build Zed 7. Open any file and start editing to see diagnostics in realtime. https://github.com/user-attachments/assets/0ef6ec41-e4fa-4539-8f2c-6be0d8be4129 --------- Co-authored-by: Kirill Bulatov <mail4score@gmail.com> Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
04cd3fcd23
commit
7aa70a4858
24 changed files with 1408 additions and 124 deletions
|
@ -4,14 +4,15 @@ use crate::{
|
|||
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
|
||||
DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
|
||||
InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
|
||||
LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState,
|
||||
LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectTransaction,
|
||||
PulledDiagnostics, ResolveState,
|
||||
lsp_store::{LocalLspStore, LspStore},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use client::proto::{self, PeerId};
|
||||
use clock::Global;
|
||||
use collections::HashSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::future;
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{
|
||||
|
@ -23,14 +24,18 @@ use language::{
|
|||
range_from_lsp, range_to_lsp,
|
||||
};
|
||||
use lsp::{
|
||||
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CompletionContext,
|
||||
CompletionListItemDefaultsEditRange, CompletionTriggerKind, DocumentHighlightKind,
|
||||
LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
|
||||
ServerCapabilities,
|
||||
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
|
||||
CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
|
||||
OneOf, RenameOptions, ServerCapabilities,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
|
||||
use std::{
|
||||
cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
|
||||
};
|
||||
use text::{BufferId, LineEnding};
|
||||
use util::{ResultExt as _, debug_panic};
|
||||
|
||||
pub use signature_help::SignatureHelp;
|
||||
|
||||
|
@ -45,7 +50,7 @@ pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOpt
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_path_to_lsp_url(path: &Path) -> Result<lsp::Url> {
|
||||
pub fn file_path_to_lsp_url(path: &Path) -> Result<lsp::Url> {
|
||||
match lsp::Url::from_file_path(path) {
|
||||
Ok(url) => Ok(url),
|
||||
Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"),
|
||||
|
@ -254,6 +259,9 @@ pub(crate) struct LinkedEditingRange {
|
|||
pub position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct GetDocumentDiagnostics {}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for PrepareRename {
|
||||
type Response = PrepareRenameResponse;
|
||||
|
@ -3656,3 +3664,627 @@ impl LspCommand for LinkedEditingRange {
|
|||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetDocumentDiagnostics {
|
||||
fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result<lsp::Diagnostic> {
|
||||
let start = diagnostic.start.context("invalid start range")?;
|
||||
let end = diagnostic.end.context("invalid end range")?;
|
||||
|
||||
let range = Range::<PointUtf16> {
|
||||
start: PointUtf16 {
|
||||
row: start.row,
|
||||
column: start.column,
|
||||
},
|
||||
end: PointUtf16 {
|
||||
row: end.row,
|
||||
column: end.column,
|
||||
},
|
||||
};
|
||||
|
||||
let data = diagnostic.data.and_then(|data| Value::from_str(&data).ok());
|
||||
let code = diagnostic.code.map(lsp::NumberOrString::String);
|
||||
|
||||
let related_information = diagnostic
|
||||
.related_information
|
||||
.into_iter()
|
||||
.map(|info| {
|
||||
let start = info.location_range_start.unwrap();
|
||||
let end = info.location_range_end.unwrap();
|
||||
|
||||
lsp::DiagnosticRelatedInformation {
|
||||
location: lsp::Location {
|
||||
range: lsp::Range {
|
||||
start: point_to_lsp(PointUtf16::new(start.row, start.column)),
|
||||
end: point_to_lsp(PointUtf16::new(end.row, end.column)),
|
||||
},
|
||||
uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(),
|
||||
},
|
||||
message: info.message.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let tags = diagnostic
|
||||
.tags
|
||||
.into_iter()
|
||||
.filter_map(|tag| match proto::LspDiagnosticTag::from_i32(tag) {
|
||||
Some(proto::LspDiagnosticTag::Unnecessary) => Some(lsp::DiagnosticTag::UNNECESSARY),
|
||||
Some(proto::LspDiagnosticTag::Deprecated) => Some(lsp::DiagnosticTag::DEPRECATED),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(lsp::Diagnostic {
|
||||
range: language::range_to_lsp(range)?,
|
||||
severity: match proto::lsp_diagnostic::Severity::from_i32(diagnostic.severity).unwrap()
|
||||
{
|
||||
proto::lsp_diagnostic::Severity::Error => Some(lsp::DiagnosticSeverity::ERROR),
|
||||
proto::lsp_diagnostic::Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
|
||||
proto::lsp_diagnostic::Severity::Information => {
|
||||
Some(lsp::DiagnosticSeverity::INFORMATION)
|
||||
}
|
||||
proto::lsp_diagnostic::Severity::Hint => Some(lsp::DiagnosticSeverity::HINT),
|
||||
_ => None,
|
||||
},
|
||||
code,
|
||||
code_description: match diagnostic.code_description {
|
||||
Some(code_description) => Some(CodeDescription {
|
||||
href: lsp::Url::parse(&code_description).unwrap(),
|
||||
}),
|
||||
None => None,
|
||||
},
|
||||
related_information: Some(related_information),
|
||||
tags: Some(tags),
|
||||
source: diagnostic.source.clone(),
|
||||
message: diagnostic.message,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result<proto::LspDiagnostic> {
|
||||
let range = language::range_from_lsp(diagnostic.range);
|
||||
let related_information = diagnostic
|
||||
.related_information
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|related_information| {
|
||||
let location_range_start =
|
||||
point_from_lsp(related_information.location.range.start).0;
|
||||
let location_range_end = point_from_lsp(related_information.location.range.end).0;
|
||||
|
||||
Ok(proto::LspDiagnosticRelatedInformation {
|
||||
location_url: Some(related_information.location.uri.to_string()),
|
||||
location_range_start: Some(proto::PointUtf16 {
|
||||
row: location_range_start.row,
|
||||
column: location_range_start.column,
|
||||
}),
|
||||
location_range_end: Some(proto::PointUtf16 {
|
||||
row: location_range_end.row,
|
||||
column: location_range_end.column,
|
||||
}),
|
||||
message: related_information.message,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let tags = diagnostic
|
||||
.tags
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|tag| match tag {
|
||||
lsp::DiagnosticTag::UNNECESSARY => proto::LspDiagnosticTag::Unnecessary,
|
||||
lsp::DiagnosticTag::DEPRECATED => proto::LspDiagnosticTag::Deprecated,
|
||||
_ => proto::LspDiagnosticTag::None,
|
||||
} as i32)
|
||||
.collect();
|
||||
|
||||
Ok(proto::LspDiagnostic {
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: range.start.0.row,
|
||||
column: range.start.0.column,
|
||||
}),
|
||||
end: Some(proto::PointUtf16 {
|
||||
row: range.end.0.row,
|
||||
column: range.end.0.column,
|
||||
}),
|
||||
severity: match diagnostic.severity {
|
||||
Some(lsp::DiagnosticSeverity::ERROR) => proto::lsp_diagnostic::Severity::Error,
|
||||
Some(lsp::DiagnosticSeverity::WARNING) => proto::lsp_diagnostic::Severity::Warning,
|
||||
Some(lsp::DiagnosticSeverity::INFORMATION) => {
|
||||
proto::lsp_diagnostic::Severity::Information
|
||||
}
|
||||
Some(lsp::DiagnosticSeverity::HINT) => proto::lsp_diagnostic::Severity::Hint,
|
||||
_ => proto::lsp_diagnostic::Severity::None,
|
||||
} as i32,
|
||||
code: diagnostic.code.as_ref().map(|code| match code {
|
||||
lsp::NumberOrString::Number(code) => code.to_string(),
|
||||
lsp::NumberOrString::String(code) => code.clone(),
|
||||
}),
|
||||
source: diagnostic.source.clone(),
|
||||
related_information,
|
||||
tags,
|
||||
code_description: diagnostic
|
||||
.code_description
|
||||
.map(|desc| desc.href.to_string()),
|
||||
message: diagnostic.message,
|
||||
data: diagnostic.data.as_ref().map(|data| data.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetDocumentDiagnostics {
|
||||
type Response = Vec<LspPullDiagnostics>;
|
||||
type LspRequest = lsp::request::DocumentDiagnosticRequest;
|
||||
type ProtoRequest = proto::GetDocumentDiagnostics;
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"Get diagnostics"
|
||||
}
|
||||
|
||||
fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
|
||||
server_capabilities
|
||||
.server_capabilities
|
||||
.diagnostic_provider
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
language_server: &Arc<LanguageServer>,
|
||||
_: &App,
|
||||
) -> Result<lsp::DocumentDiagnosticParams> {
|
||||
let identifier = match language_server.capabilities().diagnostic_provider {
|
||||
Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier,
|
||||
Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => {
|
||||
options.diagnostic_options.identifier
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(lsp::DocumentDiagnosticParams {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: file_path_to_lsp_url(path)?,
|
||||
},
|
||||
identifier,
|
||||
previous_result_id: None,
|
||||
partial_result_params: Default::default(),
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: lsp::DocumentDiagnosticReportResult,
|
||||
_: Entity<LspStore>,
|
||||
buffer: Entity<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self::Response> {
|
||||
let url = buffer.read_with(&cx, |buffer, cx| {
|
||||
buffer
|
||||
.file()
|
||||
.and_then(|file| file.as_local())
|
||||
.map(|file| {
|
||||
let abs_path = file.abs_path(cx);
|
||||
file_path_to_lsp_url(&abs_path)
|
||||
})
|
||||
.transpose()?
|
||||
.with_context(|| format!("missing url on buffer {}", buffer.remote_id()))
|
||||
})??;
|
||||
|
||||
let mut pulled_diagnostics = HashMap::default();
|
||||
match message {
|
||||
lsp::DocumentDiagnosticReportResult::Report(report) => match report {
|
||||
lsp::DocumentDiagnosticReport::Full(report) => {
|
||||
if let Some(related_documents) = report.related_documents {
|
||||
process_related_documents(
|
||||
&mut pulled_diagnostics,
|
||||
server_id,
|
||||
related_documents,
|
||||
);
|
||||
}
|
||||
process_full_diagnostics_report(
|
||||
&mut pulled_diagnostics,
|
||||
server_id,
|
||||
url,
|
||||
report.full_document_diagnostic_report,
|
||||
);
|
||||
}
|
||||
lsp::DocumentDiagnosticReport::Unchanged(report) => {
|
||||
if let Some(related_documents) = report.related_documents {
|
||||
process_related_documents(
|
||||
&mut pulled_diagnostics,
|
||||
server_id,
|
||||
related_documents,
|
||||
);
|
||||
}
|
||||
process_unchanged_diagnostics_report(
|
||||
&mut pulled_diagnostics,
|
||||
server_id,
|
||||
url,
|
||||
report.unchanged_document_diagnostic_report,
|
||||
);
|
||||
}
|
||||
},
|
||||
lsp::DocumentDiagnosticReportResult::Partial(report) => {
|
||||
if let Some(related_documents) = report.related_documents {
|
||||
process_related_documents(
|
||||
&mut pulled_diagnostics,
|
||||
server_id,
|
||||
related_documents,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pulled_diagnostics.into_values().collect())
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentDiagnostics {
|
||||
proto::GetDocumentDiagnostics {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::GetDocumentDiagnostics,
|
||||
_: Entity<LspStore>,
|
||||
buffer: Entity<Buffer>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&message.version))
|
||||
})?
|
||||
.await?;
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Self::Response,
|
||||
_: &mut LspStore,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &mut App,
|
||||
) -> proto::GetDocumentDiagnosticsResponse {
|
||||
let pulled_diagnostics = response
|
||||
.into_iter()
|
||||
.filter_map(|diagnostics| match diagnostics {
|
||||
LspPullDiagnostics::Default => None,
|
||||
LspPullDiagnostics::Response {
|
||||
server_id,
|
||||
uri,
|
||||
diagnostics,
|
||||
} => {
|
||||
let mut changed = false;
|
||||
let (diagnostics, result_id) = match diagnostics {
|
||||
PulledDiagnostics::Unchanged { result_id } => (Vec::new(), Some(result_id)),
|
||||
PulledDiagnostics::Changed {
|
||||
result_id,
|
||||
diagnostics,
|
||||
} => {
|
||||
changed = true;
|
||||
(diagnostics, result_id)
|
||||
}
|
||||
};
|
||||
Some(proto::PulledDiagnostics {
|
||||
changed,
|
||||
result_id,
|
||||
uri: uri.to_string(),
|
||||
server_id: server_id.to_proto(),
|
||||
diagnostics: diagnostics
|
||||
.into_iter()
|
||||
.filter_map(|diagnostic| {
|
||||
GetDocumentDiagnostics::serialize_lsp_diagnostic(diagnostic)
|
||||
.context("serializing diagnostics")
|
||||
.log_err()
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
proto::GetDocumentDiagnosticsResponse { pulled_diagnostics }
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
response: proto::GetDocumentDiagnosticsResponse,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> Result<Self::Response> {
|
||||
let pulled_diagnostics = response
|
||||
.pulled_diagnostics
|
||||
.into_iter()
|
||||
.filter_map(|diagnostics| {
|
||||
Some(LspPullDiagnostics::Response {
|
||||
server_id: LanguageServerId::from_proto(diagnostics.server_id),
|
||||
uri: lsp::Url::from_str(diagnostics.uri.as_str()).log_err()?,
|
||||
diagnostics: if diagnostics.changed {
|
||||
PulledDiagnostics::Unchanged {
|
||||
result_id: diagnostics.result_id?,
|
||||
}
|
||||
} else {
|
||||
PulledDiagnostics::Changed {
|
||||
result_id: diagnostics.result_id,
|
||||
diagnostics: diagnostics
|
||||
.diagnostics
|
||||
.into_iter()
|
||||
.filter_map(|diagnostic| {
|
||||
GetDocumentDiagnostics::deserialize_lsp_diagnostic(diagnostic)
|
||||
.context("deserializing diagnostics")
|
||||
.log_err()
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(pulled_diagnostics)
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_related_documents(
|
||||
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
|
||||
server_id: LanguageServerId,
|
||||
documents: impl IntoIterator<Item = (lsp::Url, lsp::DocumentDiagnosticReportKind)>,
|
||||
) {
|
||||
for (url, report_kind) in documents {
|
||||
match report_kind {
|
||||
lsp::DocumentDiagnosticReportKind::Full(report) => {
|
||||
process_full_diagnostics_report(diagnostics, server_id, url, report)
|
||||
}
|
||||
lsp::DocumentDiagnosticReportKind::Unchanged(report) => {
|
||||
process_unchanged_diagnostics_report(diagnostics, server_id, url, report)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_unchanged_diagnostics_report(
|
||||
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
|
||||
server_id: LanguageServerId,
|
||||
uri: lsp::Url,
|
||||
report: lsp::UnchangedDocumentDiagnosticReport,
|
||||
) {
|
||||
let result_id = report.result_id;
|
||||
match diagnostics.entry(uri.clone()) {
|
||||
hash_map::Entry::Occupied(mut o) => match o.get_mut() {
|
||||
LspPullDiagnostics::Default => {
|
||||
o.insert(LspPullDiagnostics::Response {
|
||||
server_id,
|
||||
uri,
|
||||
diagnostics: PulledDiagnostics::Unchanged { result_id },
|
||||
});
|
||||
}
|
||||
LspPullDiagnostics::Response {
|
||||
server_id: existing_server_id,
|
||||
uri: existing_uri,
|
||||
diagnostics: existing_diagnostics,
|
||||
} => {
|
||||
if server_id != *existing_server_id || &uri != existing_uri {
|
||||
debug_panic!(
|
||||
"Unexpected state: file {uri} has two different sets of diagnostics reported"
|
||||
);
|
||||
}
|
||||
match existing_diagnostics {
|
||||
PulledDiagnostics::Unchanged { .. } => {
|
||||
*existing_diagnostics = PulledDiagnostics::Unchanged { result_id };
|
||||
}
|
||||
PulledDiagnostics::Changed { .. } => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert(LspPullDiagnostics::Response {
|
||||
server_id,
|
||||
uri,
|
||||
diagnostics: PulledDiagnostics::Unchanged { result_id },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_full_diagnostics_report(
|
||||
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
|
||||
server_id: LanguageServerId,
|
||||
uri: lsp::Url,
|
||||
report: lsp::FullDocumentDiagnosticReport,
|
||||
) {
|
||||
let result_id = report.result_id;
|
||||
match diagnostics.entry(uri.clone()) {
|
||||
hash_map::Entry::Occupied(mut o) => match o.get_mut() {
|
||||
LspPullDiagnostics::Default => {
|
||||
o.insert(LspPullDiagnostics::Response {
|
||||
server_id,
|
||||
uri,
|
||||
diagnostics: PulledDiagnostics::Changed {
|
||||
result_id,
|
||||
diagnostics: report.items,
|
||||
},
|
||||
});
|
||||
}
|
||||
LspPullDiagnostics::Response {
|
||||
server_id: existing_server_id,
|
||||
uri: existing_uri,
|
||||
diagnostics: existing_diagnostics,
|
||||
} => {
|
||||
if server_id != *existing_server_id || &uri != existing_uri {
|
||||
debug_panic!(
|
||||
"Unexpected state: file {uri} has two different sets of diagnostics reported"
|
||||
);
|
||||
}
|
||||
match existing_diagnostics {
|
||||
PulledDiagnostics::Unchanged { .. } => {
|
||||
*existing_diagnostics = PulledDiagnostics::Changed {
|
||||
result_id,
|
||||
diagnostics: report.items,
|
||||
};
|
||||
}
|
||||
PulledDiagnostics::Changed {
|
||||
result_id: existing_result_id,
|
||||
diagnostics: existing_diagnostics,
|
||||
} => {
|
||||
if result_id.is_some() {
|
||||
*existing_result_id = result_id;
|
||||
}
|
||||
existing_diagnostics.extend(report.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert(LspPullDiagnostics::Response {
|
||||
server_id,
|
||||
uri,
|
||||
diagnostics: PulledDiagnostics::Changed {
|
||||
result_id,
|
||||
diagnostics: report.items,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lsp::{DiagnosticSeverity, DiagnosticTag};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_lsp_diagnostic() {
|
||||
let lsp_diagnostic = lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position::new(0, 1),
|
||||
end: lsp::Position::new(2, 3),
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
code: Some(lsp::NumberOrString::String("E001".to_string())),
|
||||
source: Some("test-source".to_string()),
|
||||
message: "Test error message".to_string(),
|
||||
related_information: None,
|
||||
tags: Some(vec![DiagnosticTag::DEPRECATED]),
|
||||
code_description: None,
|
||||
data: Some(json!({"detail": "test detail"})),
|
||||
};
|
||||
|
||||
let proto_diagnostic =
|
||||
GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone())
|
||||
.expect("Failed to serialize diagnostic");
|
||||
|
||||
let start = proto_diagnostic.start.unwrap();
|
||||
let end = proto_diagnostic.end.unwrap();
|
||||
assert_eq!(start.row, 0);
|
||||
assert_eq!(start.column, 1);
|
||||
assert_eq!(end.row, 2);
|
||||
assert_eq!(end.column, 3);
|
||||
assert_eq!(
|
||||
proto_diagnostic.severity,
|
||||
proto::lsp_diagnostic::Severity::Error as i32
|
||||
);
|
||||
assert_eq!(proto_diagnostic.code, Some("E001".to_string()));
|
||||
assert_eq!(proto_diagnostic.source, Some("test-source".to_string()));
|
||||
assert_eq!(proto_diagnostic.message, "Test error message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_lsp_diagnostic() {
|
||||
let proto_diagnostic = proto::LspDiagnostic {
|
||||
start: Some(proto::PointUtf16 { row: 0, column: 1 }),
|
||||
end: Some(proto::PointUtf16 { row: 2, column: 3 }),
|
||||
severity: proto::lsp_diagnostic::Severity::Warning as i32,
|
||||
code: Some("ERR".to_string()),
|
||||
source: Some("Prism".to_string()),
|
||||
message: "assigned but unused variable - a".to_string(),
|
||||
related_information: vec![],
|
||||
tags: vec![],
|
||||
code_description: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic)
|
||||
.expect("Failed to deserialize diagnostic");
|
||||
|
||||
assert_eq!(lsp_diagnostic.range.start.line, 0);
|
||||
assert_eq!(lsp_diagnostic.range.start.character, 1);
|
||||
assert_eq!(lsp_diagnostic.range.end.line, 2);
|
||||
assert_eq!(lsp_diagnostic.range.end.character, 3);
|
||||
assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING));
|
||||
assert_eq!(
|
||||
lsp_diagnostic.code,
|
||||
Some(lsp::NumberOrString::String("ERR".to_string()))
|
||||
);
|
||||
assert_eq!(lsp_diagnostic.source, Some("Prism".to_string()));
|
||||
assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_related_information() {
|
||||
let related_info = lsp::DiagnosticRelatedInformation {
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::parse("file:///test.rs").unwrap(),
|
||||
range: lsp::Range {
|
||||
start: lsp::Position::new(1, 1),
|
||||
end: lsp::Position::new(1, 5),
|
||||
},
|
||||
},
|
||||
message: "Related info message".to_string(),
|
||||
};
|
||||
|
||||
let lsp_diagnostic = lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position::new(0, 0),
|
||||
end: lsp::Position::new(0, 1),
|
||||
},
|
||||
severity: Some(DiagnosticSeverity::INFORMATION),
|
||||
code: None,
|
||||
source: Some("Prism".to_string()),
|
||||
message: "assigned but unused variable - a".to_string(),
|
||||
related_information: Some(vec![related_info]),
|
||||
tags: None,
|
||||
code_description: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic)
|
||||
.expect("Failed to serialize diagnostic");
|
||||
|
||||
assert_eq!(proto_diagnostic.related_information.len(), 1);
|
||||
let related = &proto_diagnostic.related_information[0];
|
||||
assert_eq!(related.location_url, Some("file:///test.rs".to_string()));
|
||||
assert_eq!(related.message, "Related info message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_ranges() {
|
||||
let proto_diagnostic = proto::LspDiagnostic {
|
||||
start: None,
|
||||
end: Some(proto::PointUtf16 { row: 2, column: 3 }),
|
||||
severity: proto::lsp_diagnostic::Severity::Error as i32,
|
||||
code: None,
|
||||
source: None,
|
||||
message: "Test message".to_string(),
|
||||
related_information: vec![],
|
||||
tags: vec![],
|
||||
code_description: None,
|
||||
data: None,
|
||||
};
|
||||
|
||||
let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue