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:
parent
672a1dd553
commit
ba59305510
20 changed files with 520 additions and 1071 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue