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:
Vitaly Slobodin 2025-06-05 21:42:52 +02:00 committed by GitHub
parent 04cd3fcd23
commit 7aa70a4858
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1408 additions and 124 deletions

View file

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

View file

@ -4,7 +4,8 @@ pub mod rust_analyzer_ext;
use crate::{
CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
LspAction, LspPullDiagnostics, ProjectItem, ProjectPath, ProjectTransaction, ResolveState,
Symbol, ToolchainStore,
buffer_store::{BufferStore, BufferStoreEvent},
environment::ProjectEnvironment,
lsp_command::{self, *},
@ -39,9 +40,9 @@ use http_client::HttpClient;
use itertools::Itertools as _;
use language::{
Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, LanguageName, LanguageRegistry,
LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName,
LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch,
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
},
@ -252,6 +253,10 @@ impl LocalLspStore {
let this = self.weak.clone();
let pending_workspace_folders = pending_workspace_folders.clone();
let fs = self.fs.clone();
let pull_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.lsp_pull_diagnostics_debounce_ms
.is_some();
cx.spawn(async move |cx| {
let result = async {
let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?;
@ -282,7 +287,8 @@ impl LocalLspStore {
}
let initialization_params = cx.update(|cx| {
let mut params = language_server.default_initialize_params(cx);
let mut params =
language_server.default_initialize_params(pull_diagnostics, cx);
params.initialization_options = initialization_options;
adapter.adapter.prepare_initialize_params(params, cx)
})??;
@ -474,8 +480,14 @@ impl LocalLspStore {
this.merge_diagnostics(
server_id,
params,
DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources,
|diagnostic, cx| adapter.retain_old_diagnostic(diagnostic, cx),
|diagnostic, cx| match diagnostic.source_kind {
DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => {
adapter.retain_old_diagnostic(diagnostic, cx)
}
DiagnosticSourceKind::Pulled => true,
},
cx,
)
.log_err();
@ -851,6 +863,28 @@ impl LocalLspStore {
})
.detach();
language_server
.on_request::<lsp::request::WorkspaceDiagnosticRefresh, _, _>({
let this = this.clone();
move |(), cx| {
let this = this.clone();
let mut cx = cx.clone();
async move {
this.update(&mut cx, |this, cx| {
cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
this.downstream_client.as_ref().map(|(client, project_id)| {
client.send(proto::RefreshDocumentsDiagnostics {
project_id: *project_id,
})
})
})?
.transpose()?;
Ok(())
}
}
})
.detach();
language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = this.clone();
@ -1869,8 +1903,7 @@ impl LocalLspStore {
);
}
let uri = lsp::Url::from_file_path(abs_path)
.map_err(|()| anyhow!("failed to convert abs path to uri"))?;
let uri = file_path_to_lsp_url(abs_path)?;
let text_document = lsp::TextDocumentIdentifier::new(uri);
let lsp_edits = {
@ -1934,8 +1967,7 @@ impl LocalLspStore {
let logger = zlog::scoped!("lsp_format");
zlog::info!(logger => "Formatting via LSP");
let uri = lsp::Url::from_file_path(abs_path)
.map_err(|()| anyhow!("failed to convert abs path to uri"))?;
let uri = file_path_to_lsp_url(abs_path)?;
let text_document = lsp::TextDocumentIdentifier::new(uri);
let capabilities = &language_server.capabilities();
@ -2262,7 +2294,7 @@ impl LocalLspStore {
}
let abs_path = file.abs_path(cx);
let Some(uri) = lsp::Url::from_file_path(&abs_path).log_err() else {
let Some(uri) = file_path_to_lsp_url(&abs_path).log_err() else {
return;
};
let initial_snapshot = buffer.text_snapshot();
@ -3447,6 +3479,7 @@ pub enum LspStoreEvent {
edits: Vec<(lsp::Range, Snippet)>,
most_recent_edit: clock::Lamport,
},
RefreshDocumentsDiagnostics,
}
#[derive(Clone, Debug, Serialize)]
@ -3494,6 +3527,7 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers);
client.add_entity_request_handler(Self::handle_rename_project_entry);
client.add_entity_request_handler(Self::handle_language_server_id_for_name);
client.add_entity_request_handler(Self::handle_refresh_documents_diagnostics);
client.add_entity_request_handler(Self::handle_lsp_command::<GetCodeActions>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetCompletions>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetHover>);
@ -3521,6 +3555,7 @@ impl LspStore {
client.add_entity_request_handler(
Self::handle_lsp_command::<lsp_ext_command::SwitchSourceHeader>,
);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentDiagnostics>);
}
pub fn as_remote(&self) -> Option<&RemoteLspStore> {
@ -4043,8 +4078,7 @@ impl LspStore {
.contains_key(&buffer.read(cx).remote_id())
{
if let Some(file_url) =
lsp::Url::from_file_path(&f.abs_path(cx))
.log_err()
file_path_to_lsp_url(&f.abs_path(cx)).log_err()
{
local.unregister_buffer_from_language_servers(
&buffer, &file_url, cx,
@ -4148,7 +4182,7 @@ impl LspStore {
if let Some(abs_path) =
File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx))
{
if let Some(file_url) = lsp::Url::from_file_path(&abs_path).log_err() {
if let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() {
local_store.unregister_buffer_from_language_servers(
buffer_entity,
&file_url,
@ -5674,6 +5708,73 @@ impl LspStore {
}
}
pub fn pull_diagnostics(
&mut self,
buffer_handle: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Vec<LspPullDiagnostics>>> {
let buffer = buffer_handle.read(cx);
let buffer_id = buffer.remote_id();
if let Some((client, upstream_project_id)) = self.upstream_client() {
let request_task = client.request(proto::MultiLspQuery {
buffer_id: buffer_id.into(),
version: serialize_version(&buffer_handle.read(cx).version()),
project_id: upstream_project_id,
strategy: Some(proto::multi_lsp_query::Strategy::All(
proto::AllLanguageServers {},
)),
request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
GetDocumentDiagnostics {}.to_proto(upstream_project_id, buffer_handle.read(cx)),
)),
});
let buffer = buffer_handle.clone();
cx.spawn(async move |weak_project, cx| {
let Some(project) = weak_project.upgrade() else {
return Ok(Vec::new());
};
let responses = request_task.await?.responses;
let diagnostics = join_all(
responses
.into_iter()
.filter_map(|lsp_response| match lsp_response.response? {
proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
response,
) => Some(response),
unexpected => {
debug_panic!("Unexpected response: {unexpected:?}");
None
}
})
.map(|diagnostics_response| {
GetDocumentDiagnostics {}.response_from_proto(
diagnostics_response,
project.clone(),
buffer.clone(),
cx.clone(),
)
}),
)
.await;
Ok(diagnostics
.into_iter()
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect())
})
} else {
let all_actions_task = self.request_multiple_lsp_locally(
&buffer_handle,
None::<PointUtf16>,
GetDocumentDiagnostics {},
cx,
);
cx.spawn(async move |_, _| Ok(all_actions_task.await.into_iter().flatten().collect()))
}
}
pub fn inlay_hints(
&mut self,
buffer_handle: Entity<Buffer>,
@ -6218,7 +6319,7 @@ impl LspStore {
let worktree_id = file.worktree_id(cx);
let abs_path = file.as_local()?.abs_path(cx);
let text_document = lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(abs_path).log_err()?,
uri: file_path_to_lsp_url(&abs_path).log_err()?,
};
let local = self.as_local()?;
@ -6525,15 +6626,15 @@ impl LspStore {
path: relative_path.into(),
};
if let Some(buffer) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
let snapshot = self
.as_local_mut()
.unwrap()
.buffer_snapshot_for_lsp_version(&buffer, server_id, version, cx)?;
.buffer_snapshot_for_lsp_version(&buffer_handle, server_id, version, cx)?;
let buffer = buffer_handle.read(cx);
diagnostics.extend(
buffer
.read(cx)
.get_diagnostics(server_id)
.into_iter()
.flat_map(|diag| {
@ -6549,7 +6650,7 @@ impl LspStore {
);
self.as_local_mut().unwrap().update_buffer_diagnostics(
&buffer,
&buffer_handle,
server_id,
version,
diagnostics.clone(),
@ -7071,6 +7172,47 @@ impl LspStore {
.collect(),
})
}
Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
get_document_diagnostics,
)) => {
let get_document_diagnostics = GetDocumentDiagnostics::from_proto(
get_document_diagnostics,
this.clone(),
buffer.clone(),
cx.clone(),
)
.await?;
let all_diagnostics = this
.update(&mut cx, |project, cx| {
project.request_multiple_lsp_locally(
&buffer,
None::<PointUtf16>,
get_document_diagnostics,
cx,
)
})?
.await
.into_iter();
this.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
responses: all_diagnostics
.map(|lsp_diagnostic| proto::LspResponse {
response: Some(
proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
GetDocumentDiagnostics::response_to_proto(
lsp_diagnostic,
project,
sender_id,
&buffer_version,
cx,
),
),
),
})
.collect(),
})
}
None => anyhow::bail!("empty multi lsp query request"),
}
}
@ -7671,7 +7813,7 @@ impl LspStore {
PathEventKind::Changed => lsp::FileChangeType::CHANGED,
};
Some(lsp::FileEvent {
uri: lsp::Url::from_file_path(&event.path).ok()?,
uri: file_path_to_lsp_url(&event.path).log_err()?,
typ,
})
})
@ -7997,6 +8139,17 @@ impl LspStore {
Ok(proto::Ack {})
}
async fn handle_refresh_documents_diagnostics(
this: Entity<Self>,
_: TypedEnvelope<proto::RefreshDocumentsDiagnostics>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
this.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
})?;
Ok(proto::Ack {})
}
async fn handle_inlay_hints(
this: Entity<Self>,
envelope: TypedEnvelope<proto::InlayHints>,
@ -8719,12 +8872,14 @@ impl LspStore {
&mut self,
language_server_id: LanguageServerId,
params: lsp::PublishDiagnosticsParams,
source_kind: DiagnosticSourceKind,
disk_based_sources: &[String],
cx: &mut Context<Self>,
) -> Result<()> {
self.merge_diagnostics(
language_server_id,
params,
source_kind,
disk_based_sources,
|_, _| false,
cx,
@ -8735,6 +8890,7 @@ impl LspStore {
&mut self,
language_server_id: LanguageServerId,
mut params: lsp::PublishDiagnosticsParams,
source_kind: DiagnosticSourceKind,
disk_based_sources: &[String],
filter: F,
cx: &mut Context<Self>,
@ -8799,6 +8955,7 @@ impl LspStore {
range,
diagnostic: Diagnostic {
source: diagnostic.source.clone(),
source_kind,
code: diagnostic.code.clone(),
code_description: diagnostic
.code_description
@ -8825,6 +8982,7 @@ impl LspStore {
range,
diagnostic: Diagnostic {
source: diagnostic.source.clone(),
source_kind,
code: diagnostic.code.clone(),
code_description: diagnostic
.code_description

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use ::serde::{Deserialize, Serialize};
use gpui::WeakEntity;
use language::{CachedLspAdapter, Diagnostic};
use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind};
use lsp::LanguageServer;
use util::ResultExt as _;
@ -84,6 +84,7 @@ pub fn register_notifications(
this.merge_diagnostics(
server_id,
mapped_diagnostics,
DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources,
|diag, _| !is_inactive_region(diag),
cx,

View file

@ -1,8 +1,9 @@
use crate::{
LocationLink,
lsp_command::{
LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
location_links_from_lsp, location_links_from_proto, location_links_to_proto,
LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto,
location_link_to_proto, location_links_from_lsp, location_links_from_proto,
location_links_to_proto,
},
lsp_store::LspStore,
make_lsp_text_document_position, make_text_document_identifier,
@ -584,10 +585,7 @@ impl LspCommand for GetLspRunnables {
_: &Arc<LanguageServer>,
_: &App,
) -> Result<RunnablesParams> {
let url = match lsp::Url::from_file_path(path) {
Ok(url) => url,
Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
};
let url = file_path_to_lsp_url(path)?;
Ok(RunnablesParams {
text_document: lsp::TextDocumentIdentifier::new(url),
position: self

View file

@ -72,9 +72,9 @@ use gpui::{
};
use itertools::Itertools;
use language::{
Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
Unclipped, language_settings::InlayHintKind, proto::split_operations,
Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiagnosticSourceKind, Language,
LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList,
Transaction, Unclipped, language_settings::InlayHintKind, proto::split_operations,
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
@ -317,6 +317,7 @@ pub enum Event {
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
AgentLocationChanged,
RefreshDocumentsDiagnostics,
}
pub struct AgentLocationChanged;
@ -861,6 +862,34 @@ pub const DEFAULT_COMPLETION_CONTEXT: CompletionContext = CompletionContext {
trigger_character: None,
};
/// An LSP diagnostics associated with a certain language server.
#[derive(Clone, Debug, Default)]
pub enum LspPullDiagnostics {
#[default]
Default,
Response {
/// The id of the language server that produced diagnostics.
server_id: LanguageServerId,
/// URI of the resource,
uri: lsp::Url,
/// The diagnostics produced by this language server.
diagnostics: PulledDiagnostics,
},
}
#[derive(Clone, Debug)]
pub enum PulledDiagnostics {
Unchanged {
/// An ID the current pulled batch for this file.
/// If given, can be used to query workspace diagnostics partially.
result_id: String,
},
Changed {
result_id: Option<String>,
diagnostics: Vec<lsp::Diagnostic>,
},
}
impl Project {
pub fn init_settings(cx: &mut App) {
WorktreeSettings::register(cx);
@ -2785,6 +2814,9 @@ impl Project {
}
LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
LspStoreEvent::RefreshDocumentsDiagnostics => {
cx.emit(Event::RefreshDocumentsDiagnostics)
}
LspStoreEvent::LanguageServerPrompt(prompt) => {
cx.emit(Event::LanguageServerPrompt(prompt.clone()))
}
@ -3686,6 +3718,35 @@ impl Project {
})
}
pub fn document_diagnostics(
&mut self,
buffer_handle: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Vec<LspPullDiagnostics>>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics(buffer_handle, cx)
})
}
pub fn update_diagnostics(
&mut self,
language_server_id: LanguageServerId,
source_kind: DiagnosticSourceKind,
params: lsp::PublishDiagnosticsParams,
disk_based_sources: &[String],
cx: &mut Context<Self>,
) -> Result<(), anyhow::Error> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
language_server_id,
params,
source_kind,
disk_based_sources,
cx,
)
})
}
pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
let (result_tx, result_rx) = smol::channel::unbounded();

View file

@ -127,6 +127,10 @@ pub struct DiagnosticsSettings {
/// Whether or not to include warning diagnostics.
pub include_warnings: bool,
/// Minimum time to wait before pulling diagnostics from the language server(s).
/// 0 turns the debounce off, None disables the feature.
pub lsp_pull_diagnostics_debounce_ms: Option<u64>,
/// Settings for showing inline diagnostics.
pub inline: InlineDiagnosticsSettings,
@ -209,8 +213,9 @@ impl Default for DiagnosticsSettings {
Self {
button: true,
include_warnings: true,
inline: Default::default(),
cargo: Default::default(),
lsp_pull_diagnostics_debounce_ms: Some(30),
inline: InlineDiagnosticsSettings::default(),
cargo: None,
}
}
}

View file

@ -1332,6 +1332,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@ -1349,6 +1350,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@ -1439,6 +1441,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@ -1456,6 +1459,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
DiagnosticSourceKind::Pushed,
&[],
cx,
)
@ -1633,7 +1637,8 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
message: "undefined variable 'A'".to_string(),
group_id: 0,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
}]
)
@ -2149,7 +2154,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 1,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
},
DiagnosticEntry {
@ -2161,7 +2167,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 2,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
}
]
@ -2227,7 +2234,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 4,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -2239,7 +2247,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 3,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
}
]
@ -2319,7 +2328,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 6,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -2331,7 +2341,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 5,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
}
]
@ -2372,7 +2383,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 1".to_string(),
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
},
DiagnosticEntry {
@ -2381,7 +2393,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 2".to_string(),
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
},
],
@ -2435,7 +2448,8 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
severity: DiagnosticSeverity::ERROR,
is_primary: true,
message: "syntax error a1".to_string(),
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
}],
cx,
@ -2452,7 +2466,8 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
severity: DiagnosticSeverity::ERROR,
is_primary: true,
message: "syntax error b1".to_string(),
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
},
}],
cx,
@ -4578,7 +4593,13 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(LanguageServerId(0), message, &[], cx)
lsp_store.update_diagnostics(
LanguageServerId(0),
message,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
})
.unwrap();
let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
@ -4595,7 +4616,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1".to_string(),
group_id: 1,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4605,7 +4627,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1 hint 1".to_string(),
group_id: 1,
is_primary: false,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4615,7 +4638,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 1".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4625,7 +4649,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 2".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4635,7 +4660,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2".to_string(),
group_id: 0,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
}
]
@ -4651,7 +4677,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 1".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4661,7 +4688,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 2".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4671,7 +4699,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2".to_string(),
group_id: 0,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
}
]
@ -4687,7 +4716,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1".to_string(),
group_id: 1,
is_primary: true,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
DiagnosticEntry {
@ -4697,7 +4727,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1 hint 1".to_string(),
group_id: 1,
is_primary: false,
..Default::default()
source_kind: DiagnosticSourceKind::Pushed,
..Diagnostic::default()
}
},
]