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
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -4399,7 +4399,6 @@ name = "diagnostics"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_metadata",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
|
@ -4409,7 +4408,6 @@ dependencies = [
|
|||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"linkme",
|
||||
"log",
|
||||
|
@ -4421,7 +4419,6 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"text",
|
||||
"theme",
|
||||
"ui",
|
||||
|
|
|
@ -935,22 +935,9 @@
|
|||
"max_severity": null
|
||||
},
|
||||
"rust": {
|
||||
// When enabled, Zed runs `cargo check --message-format=json`-based commands and
|
||||
// collect cargo diagnostics instead of rust-analyzer.
|
||||
"fetch_cargo_diagnostics": false,
|
||||
// A command override for fetching the cargo diagnostics.
|
||||
// First argument is the command, followed by the arguments.
|
||||
"diagnostics_fetch_command": [
|
||||
"cargo",
|
||||
"check",
|
||||
"--quiet",
|
||||
"--workspace",
|
||||
"--message-format=json",
|
||||
"--all-targets",
|
||||
"--keep-going"
|
||||
],
|
||||
// Extra environment variables to pass to the diagnostics fetch command.
|
||||
"env": {}
|
||||
// When enabled, Zed disables rust-analyzer's check on save and starts to query
|
||||
// Cargo diagnostics separately.
|
||||
"fetch_cargo_diagnostics": false
|
||||
}
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
|
|
|
@ -328,6 +328,9 @@ impl Server {
|
|||
forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
|
||||
)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtGoToParentModule>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtCancelFlycheck>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtRunFlycheck>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtClearFlycheck>)
|
||||
.add_request_handler(
|
||||
forward_read_only_project_request::<proto::LanguageServerIdForName>,
|
||||
)
|
||||
|
|
|
@ -25,7 +25,7 @@ use language::{
|
|||
use project::{
|
||||
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
lsp_store::{
|
||||
lsp_ext_command::{ExpandedMacro, LspExpandMacro},
|
||||
lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
|
||||
rust_analyzer_ext::RUST_ANALYZER_NAME,
|
||||
},
|
||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||
|
@ -2704,8 +2704,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// host
|
||||
let mut expand_request_a =
|
||||
fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
|
||||
let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
|
@ -2715,7 +2715,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
name: "test_macro_name".to_string(),
|
||||
expansion: "test_macro_expansion on the host".to_string(),
|
||||
}))
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
||||
|
@ -2738,8 +2739,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
});
|
||||
|
||||
// client
|
||||
let mut expand_request_b =
|
||||
fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
|
||||
let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
|
@ -2749,7 +2750,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
name: "test_macro_name".to_string(),
|
||||
expansion: "test_macro_expansion on the client".to_string(),
|
||||
}))
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -249,7 +249,9 @@ actions!(
|
|||
ApplyDiffHunk,
|
||||
Backspace,
|
||||
Cancel,
|
||||
CancelFlycheck,
|
||||
CancelLanguageServerWork,
|
||||
ClearFlycheck,
|
||||
ConfirmRename,
|
||||
ConfirmCompletionInsert,
|
||||
ConfirmCompletionReplace,
|
||||
|
@ -372,6 +374,7 @@ actions!(
|
|||
RevertFile,
|
||||
ReloadFile,
|
||||
Rewrap,
|
||||
RunFlycheck,
|
||||
ScrollCursorBottom,
|
||||
ScrollCursorCenter,
|
||||
ScrollCursorCenterTopBottom,
|
||||
|
|
|
@ -5,18 +5,19 @@ use gpui::{App, AppContext as _, Context, Entity, Window};
|
|||
use language::{Capability, Language, proto::serialize_anchor};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::{
|
||||
ProjectItem,
|
||||
lsp_command::location_link_from_proto,
|
||||
lsp_store::{
|
||||
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
|
||||
rust_analyzer_ext::RUST_ANALYZER_NAME,
|
||||
rust_analyzer_ext::{RUST_ANALYZER_NAME, cancel_flycheck, clear_flycheck, run_flycheck},
|
||||
},
|
||||
};
|
||||
use rpc::proto;
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{
|
||||
Editor, ExpandMacroRecursively, GoToParentModule, GotoDefinitionKind, OpenDocs,
|
||||
element::register_action, hover_links::HoverLink,
|
||||
CancelFlycheck, ClearFlycheck, Editor, ExpandMacroRecursively, GoToParentModule,
|
||||
GotoDefinitionKind, OpenDocs, RunFlycheck, element::register_action, hover_links::HoverLink,
|
||||
lsp_ext::find_specific_language_server_in_selection,
|
||||
};
|
||||
|
||||
|
@ -37,6 +38,9 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
|
|||
register_action(&editor, window, go_to_parent_module);
|
||||
register_action(&editor, window, expand_macro_recursively);
|
||||
register_action(&editor, window, open_docs);
|
||||
register_action(&editor, window, cancel_flycheck_action);
|
||||
register_action(&editor, window, run_flycheck_action);
|
||||
register_action(&editor, window, clear_flycheck_action);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,3 +304,87 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
|
|||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn cancel_flycheck_action(
|
||||
editor: &mut Editor,
|
||||
_: &CancelFlycheck,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.find_map(|selection| {
|
||||
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
|
||||
let project = project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn run_flycheck_action(
|
||||
editor: &mut Editor,
|
||||
_: &RunFlycheck,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.find_map(|selection| {
|
||||
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
|
||||
let project = project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn clear_flycheck_action(
|
||||
editor: &mut Editor,
|
||||
_: &ClearFlycheck,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.find_map(|selection| {
|
||||
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
|
||||
let project = project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
|
|
@ -260,15 +260,6 @@ impl LspAdapter for RustLspAdapter {
|
|||
Some("rust-analyzer/flycheck".into())
|
||||
}
|
||||
|
||||
fn retain_old_diagnostic(&self, previous_diagnostic: &Diagnostic, cx: &App) -> bool {
|
||||
let zed_provides_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
// Zed manages the lifecycle of cargo diagnostics when configured so.
|
||||
zed_provides_cargo_diagnostics
|
||||
&& previous_diagnostic.source.as_deref() == Some(CARGO_DIAGNOSTICS_SOURCE_NAME)
|
||||
}
|
||||
|
||||
fn process_diagnostics(
|
||||
&self,
|
||||
params: &mut lsp::PublishDiagnosticsParams,
|
||||
|
@ -516,10 +507,10 @@ impl LspAdapter for RustLspAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
let zed_provides_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
if zed_provides_cargo_diagnostics {
|
||||
if cargo_diagnostics_fetched_separately {
|
||||
let disable_check_on_save = json!({
|
||||
"checkOnSave": false,
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
environment::ProjectEnvironment,
|
||||
lsp_command::{self, *},
|
||||
lsp_store,
|
||||
manifest_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition, ManifestTree},
|
||||
prettier_store::{self, PrettierStore, PrettierStoreEvent},
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
|
@ -3396,7 +3397,7 @@ pub struct LanguageServerStatus {
|
|||
pub name: String,
|
||||
pub pending_work: BTreeMap<String, LanguageServerProgress>,
|
||||
pub has_pending_diagnostic_updates: bool,
|
||||
pub progress_tokens: HashSet<String>,
|
||||
progress_tokens: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -3449,8 +3450,14 @@ impl LspStore {
|
|||
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
|
||||
|
||||
client.add_entity_request_handler(Self::handle_lsp_ext_cancel_flycheck);
|
||||
client.add_entity_request_handler(Self::handle_lsp_ext_run_flycheck);
|
||||
client.add_entity_request_handler(Self::handle_lsp_ext_clear_flycheck);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::OpenDocs>);
|
||||
client.add_entity_request_handler(
|
||||
Self::handle_lsp_command::<lsp_ext_command::GoToParentModule>,
|
||||
);
|
||||
client.add_entity_request_handler(
|
||||
Self::handle_lsp_command::<lsp_ext_command::GetLspRunnables>,
|
||||
);
|
||||
|
@ -6236,13 +6243,6 @@ impl LspStore {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn language_server_with_name(&self, name: &str, cx: &App) -> Option<LanguageServerId> {
|
||||
self.as_local()?
|
||||
.lsp_tree
|
||||
.read(cx)
|
||||
.server_id_for_name(&LanguageServerName::from(name))
|
||||
}
|
||||
|
||||
pub fn language_servers_for_local_buffer<'a>(
|
||||
&'a self,
|
||||
buffer: &Buffer,
|
||||
|
@ -7028,37 +7028,26 @@ impl LspStore {
|
|||
mut cx: AsyncApp,
|
||||
) -> Result<proto::LanguageServerIdForNameResponse> {
|
||||
let name = &envelope.payload.name;
|
||||
match envelope.payload.buffer_id {
|
||||
Some(buffer_id) => {
|
||||
let buffer_id = BufferId::new(buffer_id)?;
|
||||
lsp_store
|
||||
.update(&mut cx, |lsp_store, cx| {
|
||||
let buffer = lsp_store.buffer_store.read(cx).get_existing(buffer_id)?;
|
||||
let server_id = buffer.update(cx, |buffer, cx| {
|
||||
lsp_store
|
||||
.language_servers_for_local_buffer(buffer, cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == name {
|
||||
Some(server.server_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
Ok(server_id)
|
||||
})?
|
||||
.map(|server_id| proto::LanguageServerIdForNameResponse {
|
||||
server_id: server_id.map(|id| id.to_proto()),
|
||||
})
|
||||
}
|
||||
None => lsp_store.update(&mut cx, |lsp_store, cx| {
|
||||
proto::LanguageServerIdForNameResponse {
|
||||
server_id: lsp_store
|
||||
.language_server_with_name(name, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
}
|
||||
}),
|
||||
}
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
lsp_store
|
||||
.update(&mut cx, |lsp_store, cx| {
|
||||
let buffer = lsp_store.buffer_store.read(cx).get_existing(buffer_id)?;
|
||||
let server_id = buffer.update(cx, |buffer, cx| {
|
||||
lsp_store
|
||||
.language_servers_for_local_buffer(buffer, cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == name {
|
||||
Some(server.server_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
Ok(server_id)
|
||||
})?
|
||||
.map(|server_id| proto::LanguageServerIdForNameResponse {
|
||||
server_id: server_id.map(|id| id.to_proto()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_rename_project_entry(
|
||||
|
@ -7282,6 +7271,77 @@ impl LspStore {
|
|||
})
|
||||
}
|
||||
|
||||
async fn handle_lsp_ext_cancel_flycheck(
|
||||
lsp_store: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::LspExtCancelFlycheck>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let server_id = LanguageServerId(envelope.payload.language_server_id as usize);
|
||||
lsp_store.update(&mut cx, |lsp_store, _| {
|
||||
if let Some(server) = lsp_store.language_server_for_id(server_id) {
|
||||
server
|
||||
.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())
|
||||
.context("handling lsp ext cancel flycheck")
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_lsp_ext_run_flycheck(
|
||||
lsp_store: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::LspExtRunFlycheck>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let server_id = LanguageServerId(envelope.payload.language_server_id as usize);
|
||||
lsp_store.update(&mut cx, |lsp_store, cx| {
|
||||
if let Some(server) = lsp_store.language_server_for_id(server_id) {
|
||||
let text_document = if envelope.payload.current_file_only {
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
lsp_store
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get(buffer_id)
|
||||
.and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)))
|
||||
.map(|path| make_text_document_identifier(&path))
|
||||
.transpose()?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
server
|
||||
.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
|
||||
&lsp_store::lsp_ext_command::RunFlycheckParams { text_document },
|
||||
)
|
||||
.context("handling lsp ext run flycheck")
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_lsp_ext_clear_flycheck(
|
||||
lsp_store: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::LspExtClearFlycheck>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let server_id = LanguageServerId(envelope.payload.language_server_id as usize);
|
||||
lsp_store.update(&mut cx, |lsp_store, _| {
|
||||
if let Some(server) = lsp_store.language_server_for_id(server_id) {
|
||||
server
|
||||
.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())
|
||||
.context("handling lsp ext clear flycheck")
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostics_started(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
|
@ -7534,7 +7594,7 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn on_lsp_progress(
|
||||
fn on_lsp_progress(
|
||||
&mut self,
|
||||
progress: lsp::ProgressParams,
|
||||
language_server_id: LanguageServerId,
|
||||
|
|
|
@ -25,9 +25,9 @@ use std::{
|
|||
use task::TaskTemplate;
|
||||
use text::{BufferId, PointUtf16, ToPointUtf16};
|
||||
|
||||
pub enum LspExpandMacro {}
|
||||
pub enum LspExtExpandMacro {}
|
||||
|
||||
impl lsp::request::Request for LspExpandMacro {
|
||||
impl lsp::request::Request for LspExtExpandMacro {
|
||||
type Params = ExpandMacroParams;
|
||||
type Result = Option<ExpandedMacro>;
|
||||
const METHOD: &'static str = "rust-analyzer/expandMacro";
|
||||
|
@ -60,7 +60,7 @@ pub struct ExpandMacro {
|
|||
#[async_trait(?Send)]
|
||||
impl LspCommand for ExpandMacro {
|
||||
type Response = ExpandedMacro;
|
||||
type LspRequest = LspExpandMacro;
|
||||
type LspRequest = LspExtExpandMacro;
|
||||
type ProtoRequest = proto::LspExtExpandMacro;
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
|
@ -753,3 +753,33 @@ impl LspCommand for GetLspRunnables {
|
|||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LspExtCancelFlycheck {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LspExtRunFlycheck {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LspExtClearFlycheck {}
|
||||
|
||||
impl lsp::notification::Notification for LspExtCancelFlycheck {
|
||||
type Params = ();
|
||||
const METHOD: &'static str = "rust-analyzer/cancelFlycheck";
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for LspExtRunFlycheck {
|
||||
type Params = RunFlycheckParams;
|
||||
const METHOD: &'static str = "rust-analyzer/runFlycheck";
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RunFlycheckParams {
|
||||
pub text_document: Option<lsp::TextDocumentIdentifier>,
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for LspExtClearFlycheck {
|
||||
type Params = ();
|
||||
const METHOD: &'static str = "rust-analyzer/clearFlycheck";
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
use ::serde::{Deserialize, Serialize};
|
||||
use gpui::{PromptLevel, WeakEntity};
|
||||
use anyhow::Context as _;
|
||||
use gpui::{App, Entity, PromptLevel, Task, WeakEntity};
|
||||
use lsp::LanguageServer;
|
||||
use rpc::proto;
|
||||
|
||||
use crate::{LanguageServerPromptRequest, LspStore, LspStoreEvent};
|
||||
use crate::{
|
||||
LanguageServerPromptRequest, LspStore, LspStoreEvent, Project, ProjectPath, lsp_store,
|
||||
};
|
||||
|
||||
pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc";
|
||||
|
@ -79,3 +83,161 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
|
|||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn cancel_flycheck(
|
||||
project: Entity<Project>,
|
||||
buffer_path: ProjectPath,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
|
||||
let lsp_store = project.read(cx).lsp_store();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
buffer_store.open_buffer(buffer_path, cx)
|
||||
})
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = buffer.await?;
|
||||
let Some(rust_analyzer_server) = project
|
||||
.update(cx, |project, cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx)
|
||||
})
|
||||
})?
|
||||
.await
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id().to_proto())?;
|
||||
|
||||
if let Some((client, project_id)) = upstream_client {
|
||||
let request = proto::LspExtCancelFlycheck {
|
||||
project_id,
|
||||
buffer_id,
|
||||
language_server_id: rust_analyzer_server.to_proto(),
|
||||
};
|
||||
client
|
||||
.request(request)
|
||||
.await
|
||||
.context("lsp ext cancel flycheck proto request")?;
|
||||
} else {
|
||||
lsp_store
|
||||
.update(cx, |lsp_store, _| {
|
||||
if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
|
||||
server.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})?
|
||||
.context("lsp ext cancel flycheck")?;
|
||||
};
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_flycheck(
|
||||
project: Entity<Project>,
|
||||
buffer_path: ProjectPath,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
|
||||
let lsp_store = project.read(cx).lsp_store();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
buffer_store.open_buffer(buffer_path, cx)
|
||||
})
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = buffer.await?;
|
||||
let Some(rust_analyzer_server) = project
|
||||
.update(cx, |project, cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx)
|
||||
})
|
||||
})?
|
||||
.await
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id().to_proto())?;
|
||||
|
||||
if let Some((client, project_id)) = upstream_client {
|
||||
let request = proto::LspExtRunFlycheck {
|
||||
project_id,
|
||||
buffer_id,
|
||||
language_server_id: rust_analyzer_server.to_proto(),
|
||||
current_file_only: false,
|
||||
};
|
||||
client
|
||||
.request(request)
|
||||
.await
|
||||
.context("lsp ext run flycheck proto request")?;
|
||||
} else {
|
||||
lsp_store
|
||||
.update(cx, |lsp_store, _| {
|
||||
if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
|
||||
server.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
|
||||
&lsp_store::lsp_ext_command::RunFlycheckParams {
|
||||
text_document: None,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})?
|
||||
.context("lsp ext run flycheck")?;
|
||||
};
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_flycheck(
|
||||
project: Entity<Project>,
|
||||
buffer_path: ProjectPath,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
|
||||
let lsp_store = project.read(cx).lsp_store();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
buffer_store.open_buffer(buffer_path, cx)
|
||||
})
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = buffer.await?;
|
||||
let Some(rust_analyzer_server) = project
|
||||
.update(cx, |project, cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx)
|
||||
})
|
||||
})?
|
||||
.await
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id().to_proto())?;
|
||||
|
||||
if let Some((client, project_id)) = upstream_client {
|
||||
let request = proto::LspExtClearFlycheck {
|
||||
project_id,
|
||||
buffer_id,
|
||||
language_server_id: rust_analyzer_server.to_proto(),
|
||||
};
|
||||
client
|
||||
.request(request)
|
||||
.await
|
||||
.context("lsp ext clear flycheck proto request")?;
|
||||
} else {
|
||||
lsp_store
|
||||
.update(cx, |lsp_store, _| {
|
||||
if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
|
||||
server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})?
|
||||
.context("lsp ext clear flycheck")?;
|
||||
};
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -247,20 +247,6 @@ impl LanguageServerTree {
|
|||
self.languages.adapter_for_name(name)
|
||||
}
|
||||
|
||||
pub fn server_id_for_name(&self, name: &LanguageServerName) -> Option<LanguageServerId> {
|
||||
self.instances
|
||||
.values()
|
||||
.flat_map(|instance| instance.roots.values())
|
||||
.flatten()
|
||||
.find_map(|(server_name, (data, _))| {
|
||||
if server_name == name {
|
||||
data.id.get().copied()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn adapters_for_language(
|
||||
&self,
|
||||
settings_location: SettingsLocation,
|
||||
|
|
|
@ -4748,42 +4748,6 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn language_server_with_name(
|
||||
&self,
|
||||
name: &str,
|
||||
cx: &App,
|
||||
) -> Task<Option<LanguageServerId>> {
|
||||
if self.is_local() {
|
||||
Task::ready(self.lsp_store.read(cx).language_server_with_name(name, cx))
|
||||
} else if let Some(project_id) = self.remote_id() {
|
||||
let request = self.client.request(proto::LanguageServerIdForName {
|
||||
project_id,
|
||||
buffer_id: None,
|
||||
name: name.to_string(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let response = request.await.log_err()?;
|
||||
response.server_id.map(LanguageServerId::from_proto)
|
||||
})
|
||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||
let request =
|
||||
ssh_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.request(proto::LanguageServerIdForName {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
buffer_id: None,
|
||||
name: name.to_string(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let response = request.await.log_err()?;
|
||||
response.server_id.map(LanguageServerId::from_proto)
|
||||
})
|
||||
} else {
|
||||
Task::ready(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language_server_id_for_name(
|
||||
&self,
|
||||
buffer: &Buffer,
|
||||
|
@ -4805,7 +4769,7 @@ impl Project {
|
|||
} else if let Some(project_id) = self.remote_id() {
|
||||
let request = self.client.request(proto::LanguageServerIdForName {
|
||||
project_id,
|
||||
buffer_id: Some(buffer.remote_id().to_proto()),
|
||||
buffer_id: buffer.remote_id().to_proto(),
|
||||
name: name.to_string(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
|
@ -4819,7 +4783,7 @@ impl Project {
|
|||
.proto_client()
|
||||
.request(proto::LanguageServerIdForName {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
buffer_id: Some(buffer.remote_id().to_proto()),
|
||||
buffer_id: buffer.remote_id().to_proto(),
|
||||
name: name.to_string(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
|
|
|
@ -155,37 +155,12 @@ pub struct InlineDiagnosticsSettings {
|
|||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CargoDiagnosticsSettings {
|
||||
/// When enabled, Zed runs `cargo check --message-format=json`-based commands and
|
||||
/// collect cargo diagnostics instead of rust-analyzer.
|
||||
/// When enabled, Zed disables rust-analyzer's check on save and starts to query
|
||||
/// Cargo diagnostics separately.
|
||||
///
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub fetch_cargo_diagnostics: bool,
|
||||
|
||||
/// A command override for fetching the cargo diagnostics.
|
||||
/// First argument is the command, followed by the arguments.
|
||||
///
|
||||
/// Default: ["cargo", "check", "--quiet", "--workspace", "--message-format=json", "--all-targets", "--keep-going"]
|
||||
#[serde(default = "default_diagnostics_fetch_command")]
|
||||
pub diagnostics_fetch_command: Vec<String>,
|
||||
|
||||
/// Extra environment variables to pass to the diagnostics fetch command.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn default_diagnostics_fetch_command() -> Vec<String> {
|
||||
vec![
|
||||
"cargo".to_string(),
|
||||
"check".to_string(),
|
||||
"--quiet".to_string(),
|
||||
"--workspace".to_string(),
|
||||
"--message-format=json".to_string(),
|
||||
"--all-targets".to_string(),
|
||||
"--keep-going".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
|
@ -706,7 +706,7 @@ message LspResponse {
|
|||
|
||||
message LanguageServerIdForName {
|
||||
uint64 project_id = 1;
|
||||
optional uint64 buffer_id = 2;
|
||||
uint64 buffer_id = 2;
|
||||
string name = 3;
|
||||
}
|
||||
|
||||
|
@ -728,3 +728,22 @@ message LspRunnable {
|
|||
bytes task_template = 1;
|
||||
optional LocationLink location = 2;
|
||||
}
|
||||
|
||||
message LspExtCancelFlycheck {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
uint64 language_server_id = 3;
|
||||
}
|
||||
|
||||
message LspExtRunFlycheck {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
uint64 language_server_id = 3;
|
||||
bool current_file_only = 4;
|
||||
}
|
||||
|
||||
message LspExtClearFlycheck {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
uint64 language_server_id = 3;
|
||||
}
|
||||
|
|
|
@ -381,7 +381,10 @@ message Envelope {
|
|||
DebugRequest debug_request = 342;
|
||||
|
||||
LspExtGoToParentModule lsp_ext_go_to_parent_module = 343;
|
||||
LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344;// current max
|
||||
LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344;
|
||||
LspExtCancelFlycheck lsp_ext_cancel_flycheck = 345;
|
||||
LspExtRunFlycheck lsp_ext_run_flycheck = 346;
|
||||
LspExtClearFlycheck lsp_ext_clear_flycheck = 347; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
|
|
@ -171,6 +171,9 @@ messages!(
|
|||
(LspExtSwitchSourceHeaderResponse, Background),
|
||||
(LspExtGoToParentModule, Background),
|
||||
(LspExtGoToParentModuleResponse, Background),
|
||||
(LspExtCancelFlycheck, Background),
|
||||
(LspExtRunFlycheck, Background),
|
||||
(LspExtClearFlycheck, Background),
|
||||
(MarkNotificationRead, Foreground),
|
||||
(MoveChannel, Foreground),
|
||||
(MultiLspQuery, Background),
|
||||
|
@ -425,6 +428,9 @@ request_messages!(
|
|||
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
||||
(LspExtGoToParentModule, LspExtGoToParentModuleResponse),
|
||||
(LspExtCancelFlycheck, Ack),
|
||||
(LspExtRunFlycheck, Ack),
|
||||
(LspExtClearFlycheck, Ack),
|
||||
(AddWorktree, AddWorktreeResponse),
|
||||
(ShutdownRemoteServer, Ack),
|
||||
(RemoveWorktree, Ack),
|
||||
|
@ -548,6 +554,9 @@ entity_messages!(
|
|||
SynchronizeContexts,
|
||||
LspExtSwitchSourceHeader,
|
||||
LspExtGoToParentModule,
|
||||
LspExtCancelFlycheck,
|
||||
LspExtRunFlycheck,
|
||||
LspExtClearFlycheck,
|
||||
LanguageServerLog,
|
||||
Toast,
|
||||
HideToast,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue