Use rust-analyzer's flycheck as source of cargo diagnostics (#29779)

Follow-up of https://github.com/zed-industries/zed/pull/29706

Instead of doing `cargo check` manually, use rust-analyzer's flycheck:
at the cost of more sophisticated check command configuration, we keep
much less code in Zed, and get a proper progress report.

User-facing UI does not change except `diagnostics_fetch_command` and
`env` settings removed from the diagnostics settings.

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-05-02 10:07:51 +03:00 committed by GitHub
parent 672a1dd553
commit ba59305510
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 520 additions and 1071 deletions

View file

@ -14,7 +14,6 @@ doctest = false
[dependencies]
anyhow.workspace = true
cargo_metadata.workspace = true
collections.workspace = true
component.workspace = true
ctor.workspace = true
@ -23,7 +22,6 @@ env_logger.workspace = true
futures.workspace = true
gpui.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
linkme.workspace = true
log.workspace = true
@ -34,7 +32,6 @@ rand.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true

View file

@ -1,603 +0,0 @@
use std::{
path::{Component, Path, Prefix},
process::Stdio,
sync::atomic::{self, AtomicUsize},
};
use cargo_metadata::{
Message,
diagnostic::{Applicability, Diagnostic as CargoDiagnostic, DiagnosticLevel, DiagnosticSpan},
};
use collections::HashMap;
use gpui::{AppContext, Entity, Task};
use itertools::Itertools as _;
use language::Diagnostic;
use project::{
Worktree, lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME,
project_settings::ProjectSettings,
};
use serde::{Deserialize, Serialize};
use settings::Settings;
use smol::{
channel::Receiver,
io::{AsyncBufReadExt, BufReader},
process::Command,
};
use ui::App;
use util::ResultExt;
use crate::ProjectDiagnosticsEditor;
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
enum CargoMessage {
Cargo(Message),
Rustc(CargoDiagnostic),
}
/// Appends formatted string to a `String`.
macro_rules! format_to {
($buf:expr) => ();
($buf:expr, $lit:literal $($arg:tt)*) => {
{
use ::std::fmt::Write as _;
// We can't do ::std::fmt::Write::write_fmt($buf, format_args!($lit $($arg)*))
// unfortunately, as that loses out on autoref behavior.
_ = $buf.write_fmt(format_args!($lit $($arg)*))
}
};
}
pub fn cargo_diagnostics_sources(
editor: &ProjectDiagnosticsEditor,
cx: &App,
) -> Vec<Entity<Worktree>> {
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.fetch_cargo_diagnostics();
if !fetch_cargo_diagnostics {
return Vec::new();
}
editor
.project
.read(cx)
.worktrees(cx)
.filter(|worktree| worktree.read(cx).entry_for_path("Cargo.toml").is_some())
.collect()
}
#[derive(Debug)]
pub enum FetchUpdate {
Diagnostic(CargoDiagnostic),
Progress(String),
}
#[derive(Debug)]
pub enum FetchStatus {
Started,
Progress { message: String },
Finished,
}
pub fn fetch_worktree_diagnostics(
worktree_root: &Path,
cx: &App,
) -> Option<(Task<()>, Receiver<FetchUpdate>)> {
let diagnostics_settings = ProjectSettings::get_global(cx)
.diagnostics
.cargo
.as_ref()
.filter(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics)?;
let command_string = diagnostics_settings
.diagnostics_fetch_command
.iter()
.join(" ");
let mut command_parts = diagnostics_settings.diagnostics_fetch_command.iter();
let mut command = Command::new(command_parts.next()?)
.args(command_parts)
.envs(diagnostics_settings.env.clone())
.current_dir(worktree_root)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.log_err()?;
let stdout = command.stdout.take()?;
let mut reader = BufReader::new(stdout);
let (tx, rx) = smol::channel::unbounded();
let error_threshold = 10;
let cargo_diagnostics_fetch_task = cx.background_spawn(async move {
let _command = command;
let mut errors = 0;
loop {
let mut line = String::new();
match reader.read_line(&mut line).await {
Ok(0) => {
return;
},
Ok(_) => {
errors = 0;
let mut deserializer = serde_json::Deserializer::from_str(&line);
deserializer.disable_recursion_limit();
let send_result = match CargoMessage::deserialize(&mut deserializer) {
Ok(CargoMessage::Cargo(Message::CompilerMessage(message))) => tx.send(FetchUpdate::Diagnostic(message.message)).await,
Ok(CargoMessage::Cargo(Message::CompilerArtifact(artifact))) => tx.send(FetchUpdate::Progress(format!("Compiled {:?}", artifact.manifest_path.parent().unwrap_or(&artifact.manifest_path)))).await,
Ok(CargoMessage::Cargo(_)) => Ok(()),
Ok(CargoMessage::Rustc(rustc_message)) => tx.send(FetchUpdate::Diagnostic(rustc_message)).await,
Err(_) => {
log::debug!("Failed to parse cargo diagnostics from line '{line}'");
Ok(())
},
};
if send_result.is_err() {
return;
}
},
Err(e) => {
log::error!("Failed to read line from {command_string} command output when fetching cargo diagnostics: {e}");
errors += 1;
if errors >= error_threshold {
log::error!("Failed {error_threshold} times, aborting the diagnostics fetch");
return;
}
},
}
}
});
Some((cargo_diagnostics_fetch_task, rx))
}
static CARGO_DIAGNOSTICS_FETCH_GENERATION: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct CargoFetchDiagnosticData {
generation: usize,
}
pub fn next_cargo_fetch_generation() {
CARGO_DIAGNOSTICS_FETCH_GENERATION.fetch_add(1, atomic::Ordering::Release);
}
pub fn is_outdated_cargo_fetch_diagnostic(diagnostic: &Diagnostic) -> bool {
if let Some(data) = diagnostic
.data
.clone()
.and_then(|data| serde_json::from_value::<CargoFetchDiagnosticData>(data).ok())
{
let current_generation = CARGO_DIAGNOSTICS_FETCH_GENERATION.load(atomic::Ordering::Acquire);
data.generation < current_generation
} else {
false
}
}
/// Converts a Rust root diagnostic to LSP form
///
/// This flattens the Rust diagnostic by:
///
/// 1. Creating a LSP diagnostic with the root message and primary span.
/// 2. Adding any labelled secondary spans to `relatedInformation`
/// 3. Categorising child diagnostics as either `SuggestedFix`es,
/// `relatedInformation` or additional message lines.
///
/// If the diagnostic has no primary span this will return `None`
///
/// Taken from https://github.com/rust-lang/rust-analyzer/blob/fe7b4f2ad96f7c13cc571f45edc2c578b35dddb4/crates/rust-analyzer/src/diagnostics/to_proto.rs#L275-L285
pub(crate) fn map_rust_diagnostic_to_lsp(
worktree_root: &Path,
cargo_diagnostic: &CargoDiagnostic,
) -> Vec<(lsp::Url, lsp::Diagnostic)> {
let primary_spans: Vec<&DiagnosticSpan> = cargo_diagnostic
.spans
.iter()
.filter(|s| s.is_primary)
.collect();
if primary_spans.is_empty() {
return Vec::new();
}
let severity = diagnostic_severity(cargo_diagnostic.level);
let mut source = String::from(CARGO_DIAGNOSTICS_SOURCE_NAME);
let mut code = cargo_diagnostic.code.as_ref().map(|c| c.code.clone());
if let Some(code_val) = &code {
// See if this is an RFC #2103 scoped lint (e.g. from Clippy)
let scoped_code: Vec<&str> = code_val.split("::").collect();
if scoped_code.len() == 2 {
source = String::from(scoped_code[0]);
code = Some(String::from(scoped_code[1]));
}
}
let mut needs_primary_span_label = true;
let mut subdiagnostics = Vec::new();
let mut tags = Vec::new();
for secondary_span in cargo_diagnostic.spans.iter().filter(|s| !s.is_primary) {
if let Some(label) = secondary_span.label.clone() {
subdiagnostics.push(lsp::DiagnosticRelatedInformation {
location: location(worktree_root, secondary_span),
message: label,
});
}
}
let mut message = cargo_diagnostic.message.clone();
for child in &cargo_diagnostic.children {
let child = map_rust_child_diagnostic(worktree_root, child);
match child {
MappedRustChildDiagnostic::SubDiagnostic(sub) => {
subdiagnostics.push(sub);
}
MappedRustChildDiagnostic::MessageLine(message_line) => {
format_to!(message, "\n{message_line}");
// These secondary messages usually duplicate the content of the
// primary span label.
needs_primary_span_label = false;
}
}
}
if let Some(code) = &cargo_diagnostic.code {
let code = code.code.as_str();
if matches!(
code,
"dead_code"
| "unknown_lints"
| "unreachable_code"
| "unused_attributes"
| "unused_imports"
| "unused_macros"
| "unused_variables"
) {
tags.push(lsp::DiagnosticTag::UNNECESSARY);
}
if matches!(code, "deprecated") {
tags.push(lsp::DiagnosticTag::DEPRECATED);
}
}
let code_description = match source.as_str() {
"rustc" => rustc_code_description(code.as_deref()),
"clippy" => clippy_code_description(code.as_deref()),
_ => None,
};
let generation = CARGO_DIAGNOSTICS_FETCH_GENERATION.load(atomic::Ordering::Acquire);
let data = Some(
serde_json::to_value(CargoFetchDiagnosticData { generation })
.expect("Serializing a regular Rust struct"),
);
primary_spans
.iter()
.flat_map(|primary_span| {
let primary_location = primary_location(worktree_root, primary_span);
let message = {
let mut message = message.clone();
if needs_primary_span_label {
if let Some(primary_span_label) = &primary_span.label {
format_to!(message, "\n{primary_span_label}");
}
}
message
};
// Each primary diagnostic span may result in multiple LSP diagnostics.
let mut diagnostics = Vec::new();
let mut related_info_macro_calls = vec![];
// If error occurs from macro expansion, add related info pointing to
// where the error originated
// Also, we would generate an additional diagnostic, so that exact place of macro
// will be highlighted in the error origin place.
let span_stack = std::iter::successors(Some(*primary_span), |span| {
Some(&span.expansion.as_ref()?.span)
});
for (i, span) in span_stack.enumerate() {
if is_dummy_macro_file(&span.file_name) {
continue;
}
// First span is the original diagnostic, others are macro call locations that
// generated that code.
let is_in_macro_call = i != 0;
let secondary_location = location(worktree_root, span);
if secondary_location == primary_location {
continue;
}
related_info_macro_calls.push(lsp::DiagnosticRelatedInformation {
location: secondary_location.clone(),
message: if is_in_macro_call {
"Error originated from macro call here".to_owned()
} else {
"Actual error occurred here".to_owned()
},
});
// For the additional in-macro diagnostic we add the inverse message pointing to the error location in code.
let information_for_additional_diagnostic =
vec![lsp::DiagnosticRelatedInformation {
location: primary_location.clone(),
message: "Exact error occurred here".to_owned(),
}];
let diagnostic = lsp::Diagnostic {
range: secondary_location.range,
// downgrade to hint if we're pointing at the macro
severity: Some(lsp::DiagnosticSeverity::HINT),
code: code.clone().map(lsp::NumberOrString::String),
code_description: code_description.clone(),
source: Some(source.clone()),
message: message.clone(),
related_information: Some(information_for_additional_diagnostic),
tags: if tags.is_empty() {
None
} else {
Some(tags.clone())
},
data: data.clone(),
};
diagnostics.push((secondary_location.uri, diagnostic));
}
// Emit the primary diagnostic.
diagnostics.push((
primary_location.uri.clone(),
lsp::Diagnostic {
range: primary_location.range,
severity,
code: code.clone().map(lsp::NumberOrString::String),
code_description: code_description.clone(),
source: Some(source.clone()),
message,
related_information: {
let info = related_info_macro_calls
.iter()
.cloned()
.chain(subdiagnostics.iter().cloned())
.collect::<Vec<_>>();
if info.is_empty() { None } else { Some(info) }
},
tags: if tags.is_empty() {
None
} else {
Some(tags.clone())
},
data: data.clone(),
},
));
// Emit hint-level diagnostics for all `related_information` entries such as "help"s.
// This is useful because they will show up in the user's editor, unlike
// `related_information`, which just produces hard-to-read links, at least in VS Code.
let back_ref = lsp::DiagnosticRelatedInformation {
location: primary_location,
message: "original diagnostic".to_owned(),
};
for sub in &subdiagnostics {
diagnostics.push((
sub.location.uri.clone(),
lsp::Diagnostic {
range: sub.location.range,
severity: Some(lsp::DiagnosticSeverity::HINT),
code: code.clone().map(lsp::NumberOrString::String),
code_description: code_description.clone(),
source: Some(source.clone()),
message: sub.message.clone(),
related_information: Some(vec![back_ref.clone()]),
tags: None, // don't apply modifiers again
data: data.clone(),
},
));
}
diagnostics
})
.collect()
}
fn rustc_code_description(code: Option<&str>) -> Option<lsp::CodeDescription> {
code.filter(|code| {
let mut chars = code.chars();
chars.next() == Some('E')
&& chars.by_ref().take(4).all(|c| c.is_ascii_digit())
&& chars.next().is_none()
})
.and_then(|code| {
lsp::Url::parse(&format!(
"https://doc.rust-lang.org/error-index.html#{code}"
))
.ok()
.map(|href| lsp::CodeDescription { href })
})
}
fn clippy_code_description(code: Option<&str>) -> Option<lsp::CodeDescription> {
code.and_then(|code| {
lsp::Url::parse(&format!(
"https://rust-lang.github.io/rust-clippy/master/index.html#{code}"
))
.ok()
.map(|href| lsp::CodeDescription { href })
})
}
/// Determines the LSP severity from a diagnostic
fn diagnostic_severity(level: DiagnosticLevel) -> Option<lsp::DiagnosticSeverity> {
let res = match level {
DiagnosticLevel::Ice => lsp::DiagnosticSeverity::ERROR,
DiagnosticLevel::Error => lsp::DiagnosticSeverity::ERROR,
DiagnosticLevel::Warning => lsp::DiagnosticSeverity::WARNING,
DiagnosticLevel::Note => lsp::DiagnosticSeverity::INFORMATION,
DiagnosticLevel::Help => lsp::DiagnosticSeverity::HINT,
_ => return None,
};
Some(res)
}
enum MappedRustChildDiagnostic {
SubDiagnostic(lsp::DiagnosticRelatedInformation),
MessageLine(String),
}
fn map_rust_child_diagnostic(
worktree_root: &Path,
cargo_diagnostic: &CargoDiagnostic,
) -> MappedRustChildDiagnostic {
let spans: Vec<&DiagnosticSpan> = cargo_diagnostic
.spans
.iter()
.filter(|s| s.is_primary)
.collect();
if spans.is_empty() {
// `rustc` uses these spanless children as a way to print multi-line
// messages
return MappedRustChildDiagnostic::MessageLine(cargo_diagnostic.message.clone());
}
let mut edit_map: HashMap<lsp::Url, Vec<lsp::TextEdit>> = HashMap::default();
let mut suggested_replacements = Vec::new();
for &span in &spans {
if let Some(suggested_replacement) = &span.suggested_replacement {
if !suggested_replacement.is_empty() {
suggested_replacements.push(suggested_replacement);
}
let location = location(worktree_root, span);
let edit = lsp::TextEdit::new(location.range, suggested_replacement.clone());
// Only actually emit a quickfix if the suggestion is "valid enough".
// We accept both "MaybeIncorrect" and "MachineApplicable". "MaybeIncorrect" means that
// the suggestion is *complete* (contains no placeholders where code needs to be
// inserted), but might not be what the user wants, or might need minor adjustments.
if matches!(
span.suggestion_applicability,
None | Some(Applicability::MaybeIncorrect | Applicability::MachineApplicable)
) {
edit_map.entry(location.uri).or_default().push(edit);
}
}
}
// rustc renders suggestion diagnostics by appending the suggested replacement, so do the same
// here, otherwise the diagnostic text is missing useful information.
let mut message = cargo_diagnostic.message.clone();
if !suggested_replacements.is_empty() {
message.push_str(": ");
let suggestions = suggested_replacements
.iter()
.map(|suggestion| format!("`{suggestion}`"))
.join(", ");
message.push_str(&suggestions);
}
MappedRustChildDiagnostic::SubDiagnostic(lsp::DiagnosticRelatedInformation {
location: location(worktree_root, spans[0]),
message,
})
}
/// Converts a Rust span to a LSP location
fn location(worktree_root: &Path, span: &DiagnosticSpan) -> lsp::Location {
let file_name = worktree_root.join(&span.file_name);
let uri = url_from_abs_path(&file_name);
let range = {
lsp::Range::new(
position(span, span.line_start, span.column_start.saturating_sub(1)),
position(span, span.line_end, span.column_end.saturating_sub(1)),
)
};
lsp::Location::new(uri, range)
}
/// Returns a `Url` object from a given path, will lowercase drive letters if present.
/// This will only happen when processing windows paths.
///
/// When processing non-windows path, this is essentially the same as `Url::from_file_path`.
pub(crate) fn url_from_abs_path(path: &Path) -> lsp::Url {
let url = lsp::Url::from_file_path(path).unwrap();
match path.components().next() {
Some(Component::Prefix(prefix))
if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)) =>
{
// Need to lowercase driver letter
}
_ => return url,
}
let driver_letter_range = {
let (scheme, drive_letter, _rest) = match url.as_str().splitn(3, ':').collect_tuple() {
Some(it) => it,
None => return url,
};
let start = scheme.len() + ':'.len_utf8();
start..(start + drive_letter.len())
};
// Note: lowercasing the `path` itself doesn't help, the `Url::parse`
// machinery *also* canonicalizes the drive letter. So, just massage the
// string in place.
let mut url: String = url.into();
url[driver_letter_range].make_ascii_lowercase();
lsp::Url::parse(&url).unwrap()
}
fn position(
span: &DiagnosticSpan,
line_number: usize,
column_offset_utf32: usize,
) -> lsp::Position {
let line_index = line_number - span.line_start;
let column_offset_encoded = match span.text.get(line_index) {
// Fast path.
Some(line) if line.text.is_ascii() => column_offset_utf32,
Some(line) => {
let line_prefix_len = line
.text
.char_indices()
.take(column_offset_utf32)
.last()
.map(|(pos, c)| pos + c.len_utf8())
.unwrap_or(0);
let line_prefix = &line.text[..line_prefix_len];
line_prefix.len()
}
None => column_offset_utf32,
};
lsp::Position {
line: (line_number as u32).saturating_sub(1),
character: column_offset_encoded as u32,
}
}
/// Checks whether a file name is from macro invocation and does not refer to an actual file.
fn is_dummy_macro_file(file_name: &str) -> bool {
file_name.starts_with('<') && file_name.ends_with('>')
}
/// Extracts a suitable "primary" location from a rustc diagnostic.
///
/// This takes locations pointing into the standard library, or generally outside the current
/// workspace into account and tries to avoid those, in case macros are involved.
fn primary_location(worktree_root: &Path, span: &DiagnosticSpan) -> lsp::Location {
let span_stack = std::iter::successors(Some(span), |span| Some(&span.expansion.as_ref()?.span));
for span in span_stack.clone() {
let abs_path = worktree_root.join(&span.file_name);
if !is_dummy_macro_file(&span.file_name) && abs_path.starts_with(worktree_root) {
return location(worktree_root, span);
}
}
// Fall back to the outermost macro invocation if no suitable span comes up.
let last_span = span_stack.last().unwrap();
location(worktree_root, last_span)
}

View file

@ -1,4 +1,3 @@
mod cargo;
pub mod items;
mod toolbar_controls;
@ -8,18 +7,14 @@ mod diagnostic_renderer;
mod diagnostics_tests;
use anyhow::Result;
use cargo::{
FetchStatus, FetchUpdate, cargo_diagnostics_sources, fetch_worktree_diagnostics,
is_outdated_cargo_fetch_diagnostic, map_rust_diagnostic_to_lsp, next_cargo_fetch_generation,
url_from_abs_path,
};
use collections::{BTreeSet, HashMap, HashSet};
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
scroll::Autoscroll,
};
use futures::future::join_all;
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
@ -28,10 +23,10 @@ use gpui::{
use language::{
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
};
use lsp::{DiagnosticSeverity, LanguageServerId};
use lsp::DiagnosticSeverity;
use project::{
DiagnosticSummary, Project, ProjectPath, Worktree,
lsp_store::rust_analyzer_ext::{CARGO_DIAGNOSTICS_SOURCE_NAME, RUST_ANALYZER_NAME},
DiagnosticSummary, Project, ProjectPath,
lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
project_settings::ProjectSettings,
};
use settings::Settings;
@ -84,8 +79,9 @@ pub(crate) struct ProjectDiagnosticsEditor {
}
struct CargoDiagnosticsFetchState {
task: Option<Task<()>>,
rust_analyzer: Option<LanguageServerId>,
fetch_task: Option<Task<()>>,
cancel_task: Option<Task<()>>,
diagnostic_sources: Arc<Vec<ProjectPath>>,
}
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
@ -252,8 +248,9 @@ impl ProjectDiagnosticsEditor {
paths_to_update: Default::default(),
update_excerpts_task: None,
cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
task: None,
rust_analyzer: None,
fetch_task: None,
cancel_task: None,
diagnostic_sources: Arc::new(Vec::new()),
},
_subscription: project_event_subscription,
};
@ -346,7 +343,7 @@ impl ProjectDiagnosticsEditor {
.fetch_cargo_diagnostics();
if fetch_cargo_diagnostics {
if self.cargo_diagnostics_fetch.task.is_some() {
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
self.stop_cargo_diagnostics_fetch(cx);
} else {
self.update_all_diagnostics(window, cx);
@ -375,300 +372,63 @@ impl ProjectDiagnosticsEditor {
}
fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let cargo_diagnostics_sources = cargo_diagnostics_sources(self, cx);
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
if cargo_diagnostics_sources.is_empty() {
self.update_all_excerpts(window, cx);
} else {
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), window, cx);
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
}
}
fn fetch_cargo_diagnostics(
&mut self,
diagnostics_sources: Arc<Vec<Entity<Worktree>>>,
window: &mut Window,
diagnostics_sources: Arc<Vec<ProjectPath>>,
cx: &mut Context<Self>,
) {
self.cargo_diagnostics_fetch.task = Some(cx.spawn_in(window, async move |editor, cx| {
let rust_analyzer_server = editor
.update(cx, |editor, cx| {
editor
.project
.read(cx)
.language_server_with_name(RUST_ANALYZER_NAME, cx)
})
.ok();
let rust_analyzer_server = match rust_analyzer_server {
Some(rust_analyzer_server) => rust_analyzer_server.await,
None => None,
};
let project = self.project.clone();
self.cargo_diagnostics_fetch.cancel_task = None;
self.cargo_diagnostics_fetch.fetch_task = None;
self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
return;
}
let mut worktree_diagnostics_tasks = Vec::new();
let mut paths_with_reported_cargo_diagnostics = HashSet::default();
if let Some(rust_analyzer_server) = rust_analyzer_server {
let can_continue = editor
.update(cx, |editor, cx| {
editor.cargo_diagnostics_fetch.rust_analyzer = Some(rust_analyzer_server);
let status_inserted =
editor
.project
.read(cx)
.lsp_store()
.update(cx, |lsp_store, cx| {
if let Some(rust_analyzer_status) = lsp_store
.language_server_statuses
.get_mut(&rust_analyzer_server)
{
rust_analyzer_status
.progress_tokens
.insert(fetch_cargo_diagnostics_token());
paths_with_reported_cargo_diagnostics.extend(editor.diagnostics.iter().filter_map(|(buffer_id, diagnostics)| {
if diagnostics.iter().any(|d| d.diagnostic.source.as_deref() == Some(CARGO_DIAGNOSTICS_SOURCE_NAME)) {
Some(*buffer_id)
} else {
None
}
}).filter_map(|buffer_id| {
let buffer = lsp_store.buffer_store().read(cx).get(buffer_id)?;
let path = buffer.read(cx).file()?.as_local()?.abs_path(cx);
Some(url_from_abs_path(&path))
}));
true
} else {
false
}
});
if status_inserted {
editor.update_cargo_fetch_status(FetchStatus::Started, cx);
next_cargo_fetch_generation();
true
} else {
false
}
self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
let mut fetch_tasks = Vec::new();
for buffer_path in diagnostics_sources.iter().cloned() {
if cx
.update(|cx| {
fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
})
.unwrap_or(false);
if can_continue {
for worktree in diagnostics_sources.iter() {
if let Some(((_task, worktree_diagnostics), worktree_root)) = cx
.update(|_, cx| {
let worktree_root = worktree.read(cx).abs_path();
log::info!("Fetching cargo diagnostics for {worktree_root:?}");
fetch_worktree_diagnostics(&worktree_root, cx)
.zip(Some(worktree_root))
})
.ok()
.flatten()
{
let editor = editor.clone();
worktree_diagnostics_tasks.push(cx.spawn(async move |cx| {
let _task = _task;
let mut file_diagnostics = HashMap::default();
let mut diagnostics_total = 0;
let mut updated_urls = HashSet::default();
while let Ok(fetch_update) = worktree_diagnostics.recv().await {
match fetch_update {
FetchUpdate::Diagnostic(diagnostic) => {
for (url, diagnostic) in map_rust_diagnostic_to_lsp(
&worktree_root,
&diagnostic,
) {
let file_diagnostics = file_diagnostics
.entry(url)
.or_insert_with(Vec::<lsp::Diagnostic>::new);
let i = file_diagnostics
.binary_search_by(|probe| {
probe.range.start.cmp(&diagnostic.range.start)
.then(probe.range.end.cmp(&diagnostic.range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|i| i);
file_diagnostics.insert(i, diagnostic);
}
let file_changed = file_diagnostics.len() > 1;
if file_changed {
if editor
.update_in(cx, |editor, window, cx| {
editor
.project
.read(cx)
.lsp_store()
.update(cx, |lsp_store, cx| {
for (uri, mut diagnostics) in
file_diagnostics.drain()
{
diagnostics.dedup();
diagnostics_total += diagnostics.len();
updated_urls.insert(uri.clone());
lsp_store.merge_diagnostics(
rust_analyzer_server,
lsp::PublishDiagnosticsParams {
uri,
diagnostics,
version: None,
},
&[],
|diagnostic, _| {
!is_outdated_cargo_fetch_diagnostic(diagnostic)
},
cx,
)?;
}
anyhow::Ok(())
})?;
editor.update_all_excerpts(window, cx);
anyhow::Ok(())
})
.ok()
.transpose()
.ok()
.flatten()
.is_none()
{
break;
}
}
}
FetchUpdate::Progress(message) => {
if editor
.update(cx, |editor, cx| {
editor.update_cargo_fetch_status(
FetchStatus::Progress { message },
cx,
);
})
.is_err()
{
return updated_urls;
}
}
}
}
editor
.update_in(cx, |editor, window, cx| {
editor
.project
.read(cx)
.lsp_store()
.update(cx, |lsp_store, cx| {
for (uri, mut diagnostics) in
file_diagnostics.drain()
{
diagnostics.dedup();
diagnostics_total += diagnostics.len();
updated_urls.insert(uri.clone());
lsp_store.merge_diagnostics(
rust_analyzer_server,
lsp::PublishDiagnosticsParams {
uri,
diagnostics,
version: None,
},
&[],
|diagnostic, _| {
!is_outdated_cargo_fetch_diagnostic(diagnostic)
},
cx,
)?;
}
anyhow::Ok(())
})?;
editor.update_all_excerpts(window, cx);
anyhow::Ok(())
})
.ok();
log::info!("Fetched {diagnostics_total} cargo diagnostics for worktree {worktree_root:?}");
updated_urls
}));
}
}
} else {
log::info!(
"No rust-analyzer language server found, skipping diagnostics fetch"
);
.is_err()
{
break;
}
}
let updated_urls = futures::future::join_all(worktree_diagnostics_tasks).await.into_iter().flatten().collect();
if let Some(rust_analyzer_server) = rust_analyzer_server {
let _ = join_all(fetch_tasks).await;
editor
.update_in(cx, |editor, window, cx| {
editor
.project
.read(cx)
.lsp_store()
.update(cx, |lsp_store, cx| {
for uri_to_cleanup in paths_with_reported_cargo_diagnostics.difference(&updated_urls).cloned() {
lsp_store.merge_diagnostics(
rust_analyzer_server,
lsp::PublishDiagnosticsParams {
uri: uri_to_cleanup,
diagnostics: Vec::new(),
version: None,
},
&[],
|diagnostic, _| {
!is_outdated_cargo_fetch_diagnostic(diagnostic)
},
cx,
).ok();
}
});
editor.update_all_excerpts(window, cx);
editor.stop_cargo_diagnostics_fetch(cx);
cx.notify();
.update(cx, |editor, _| {
editor.cargo_diagnostics_fetch.fetch_task = None;
})
.ok();
}
}));
}
fn update_cargo_fetch_status(&self, status: FetchStatus, cx: &mut App) {
let Some(rust_analyzer) = self.cargo_diagnostics_fetch.rust_analyzer else {
return;
};
let work_done = match status {
FetchStatus::Started => lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
title: "cargo".to_string(),
cancellable: None,
message: Some("Fetching cargo diagnostics".to_string()),
percentage: None,
}),
FetchStatus::Progress { message } => {
lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
message: Some(message),
cancellable: None,
percentage: None,
})
}
FetchStatus::Finished => {
lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message: None })
}
};
let progress = lsp::ProgressParams {
token: lsp::NumberOrString::String(fetch_cargo_diagnostics_token()),
value: lsp::ProgressParamsValue::WorkDone(work_done),
};
self.project
.read(cx)
.lsp_store()
.update(cx, |lsp_store, cx| {
lsp_store.on_lsp_progress(progress, rust_analyzer, None, cx)
});
}
fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
self.update_cargo_fetch_status(FetchStatus::Finished, cx);
self.cargo_diagnostics_fetch.task = None;
log::info!("Finished fetching cargo diagnostics");
self.cargo_diagnostics_fetch.fetch_task = None;
let mut cancel_gasks = Vec::new();
for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
.iter()
.cloned()
{
cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
}
self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
let _ = join_all(cancel_gasks).await;
log::info!("Finished fetching cargo diagnostics");
}));
}
/// Enqueue an update of all excerpts. Updates all paths that either
@ -897,6 +657,30 @@ impl ProjectDiagnosticsEditor {
})
})
}
pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.fetch_cargo_diagnostics();
if !fetch_cargo_diagnostics {
return Vec::new();
}
self.project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
entry
.path
.extension()
.and_then(|extension| extension.to_str())
== Some("rs")
})?;
self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
})
.collect()
}
}
impl Focusable for ProjectDiagnosticsEditor {
@ -1286,7 +1070,3 @@ fn is_line_blank_or_indented_less(
let line_indent = snapshot.line_indent_for_row(row);
line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
}
fn fetch_cargo_diagnostics_token() -> String {
"fetch_cargo_diagnostics".to_string()
}

View file

@ -1,6 +1,5 @@
use std::sync::Arc;
use crate::cargo::cargo_diagnostics_sources;
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
use ui::prelude::*;
@ -16,11 +15,9 @@ impl Render for ToolbarControls {
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut is_updating = false;
let cargo_diagnostics_sources = Arc::new(
self.diagnostics()
.map(|editor| cargo_diagnostics_sources(editor.read(cx), cx))
.unwrap_or_default(),
);
let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
editor.read(cx).cargo_diagnostics_sources(cx)
}));
let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
if let Some(editor) = self.diagnostics() {
@ -28,7 +25,7 @@ impl Render for ToolbarControls {
include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = if fetch_cargo_diagnostics {
diagnostics.cargo_diagnostics_fetch.task.is_some()
diagnostics.cargo_diagnostics_fetch.fetch_task.is_some()
} else {
diagnostics.update_excerpts_task.is_some()
|| diagnostics
@ -93,7 +90,6 @@ impl Render for ToolbarControls {
if fetch_cargo_diagnostics {
diagnostics.fetch_cargo_diagnostics(
cargo_diagnostics_sources,
window,
cx,
);
} else {