Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/hir-ty/src/next_solver/infer/traits.rs7
-rw-r--r--crates/project-model/src/project_json.rs41
-rw-r--r--crates/rust-analyzer/src/config.rs47
-rw-r--r--crates/rust-analyzer/src/diagnostics.rs16
-rw-r--r--crates/rust-analyzer/src/flycheck.rs500
-rw-r--r--crates/rust-analyzer/src/global_state.rs58
-rw-r--r--crates/rust-analyzer/src/handlers/notification.rs51
-rw-r--r--crates/rust-analyzer/src/main_loop.rs27
-rw-r--r--crates/rust-analyzer/src/reload.rs20
-rw-r--r--crates/rust-analyzer/src/target_spec.rs12
-rw-r--r--crates/rust-analyzer/src/test_runner.rs2
-rw-r--r--crates/toolchain/src/lib.rs3
-rw-r--r--docs/book/src/configuration_generated.md27
-rw-r--r--docs/book/src/non_cargo_based_projects.md128
-rw-r--r--editors/code/package.json4
15 files changed, 773 insertions, 170 deletions
diff --git a/crates/hir-ty/src/next_solver/infer/traits.rs b/crates/hir-ty/src/next_solver/infer/traits.rs
index 14df42dc2a..dde6234836 100644
--- a/crates/hir-ty/src/next_solver/infer/traits.rs
+++ b/crates/hir-ty/src/next_solver/infer/traits.rs
@@ -55,6 +55,13 @@ impl ObligationCause {
}
}
+impl Default for ObligationCause {
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
/// An `Obligation` represents some trait reference (e.g., `i32: Eq`) for
/// which the "impl_source" must be found. The process of finding an "impl_source" is
/// called "resolving" the `Obligation`. This process consists of
diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs
index b3478d2cfe..6938010cbd 100644
--- a/crates/project-model/src/project_json.rs
+++ b/crates/project-model/src/project_json.rs
@@ -78,6 +78,13 @@ pub struct ProjectJson {
runnables: Vec<Runnable>,
}
+impl std::ops::Index<CrateArrayIdx> for ProjectJson {
+ type Output = Crate;
+ fn index(&self, index: CrateArrayIdx) -> &Self::Output {
+ &self.crates[index.0]
+ }
+}
+
impl ProjectJson {
/// Create a new ProjectJson instance.
///
@@ -195,12 +202,11 @@ impl ProjectJson {
&self.project_root
}
- pub fn crate_by_root(&self, root: &AbsPath) -> Option<Crate> {
+ pub fn crate_by_root(&self, root: &AbsPath) -> Option<&Crate> {
self.crates
.iter()
.filter(|krate| krate.is_workspace_member)
.find(|krate| krate.root_module == root)
- .cloned()
}
/// Returns the path to the project's manifest, if it exists.
@@ -214,8 +220,17 @@ impl ProjectJson {
self.crates
.iter()
.filter(|krate| krate.is_workspace_member)
- .filter_map(|krate| krate.build.clone())
+ .filter_map(|krate| krate.build.as_ref())
.find(|build| build.build_file.as_std_path() == path)
+ .cloned()
+ }
+
+ pub fn crate_by_label(&self, label: &str) -> Option<&Crate> {
+ // this is fast enough for now, but it's unfortunate that this is O(crates).
+ self.crates
+ .iter()
+ .filter(|krate| krate.is_workspace_member)
+ .find(|krate| krate.build.as_ref().is_some_and(|build| build.label == label))
}
/// Returns the path to the project's manifest or root folder, if no manifest exists.
@@ -231,6 +246,10 @@ impl ProjectJson {
pub fn runnables(&self) -> &[Runnable] {
&self.runnables
}
+
+ pub fn runnable_template(&self, kind: RunnableKind) -> Option<&Runnable> {
+ self.runnables().iter().find(|r| r.kind == kind)
+ }
}
/// A crate points to the root module of a crate and lists the dependencies of the crate. This is
@@ -258,6 +277,12 @@ pub struct Crate {
pub build: Option<Build>,
}
+impl Crate {
+ pub fn iter_deps(&self) -> impl ExactSizeIterator<Item = CrateArrayIdx> {
+ self.deps.iter().map(|dep| dep.krate)
+ }
+}
+
/// Additional, build-specific data about a crate.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Build {
@@ -328,13 +353,21 @@ pub struct Runnable {
/// The kind of runnable.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunnableKind {
+ /// `cargo check`, basically, with human-readable output.
Check,
/// Can run a binary.
+ /// May include {label} which will get the label from the `build` section of a crate.
Run,
/// Run a single test.
+ /// May include {label} which will get the label from the `build` section of a crate.
+ /// May include {test_id} which will get the test clicked on by the user.
TestOne,
+
+ /// Template for checking a target, emitting rustc JSON diagnostics.
+ /// May include {label} which will get the label from the `build` section of a crate.
+ Flycheck,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
@@ -441,6 +474,7 @@ pub struct RunnableData {
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RunnableKindData {
+ Flycheck,
Check,
Run,
TestOne,
@@ -511,6 +545,7 @@ impl From<RunnableKindData> for RunnableKind {
RunnableKindData::Check => RunnableKind::Check,
RunnableKindData::Run => RunnableKind::Run,
RunnableKindData::TestOne => RunnableKind::TestOne,
+ RunnableKindData::Flycheck => RunnableKind::Flycheck,
}
}
}
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index e39569e108..28ac94e4de 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -480,8 +480,8 @@ config_data! {
/// Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].
///
- /// [`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`.
- /// `progress_label` is used for the title in progress indicators, whereas `files_to_watch`
+ /// [`DiscoverWorkspaceConfig`] also requires setting `progressLabel` and `filesToWatch`.
+ /// `progressLabel` is used for the title in progress indicators, whereas `filesToWatch`
/// is used to determine which build system-specific files should be watched in order to
/// reload rust-analyzer.
///
@@ -490,16 +490,17 @@ config_data! {
/// "rust-analyzer.workspace.discoverConfig": {
/// "command": [
/// "rust-project",
- /// "develop-json"
+ /// "develop-json",
+ /// "{arg}"
/// ],
- /// "progressLabel": "rust-analyzer",
+ /// "progressLabel": "buck2/rust-project",
/// "filesToWatch": [
/// "BUCK"
/// ]
/// }
/// ```
///
- /// ## On `DiscoverWorkspaceConfig::command`
+ /// ## Workspace Discovery Protocol
///
/// **Warning**: This format is provisional and subject to change.
///
@@ -870,10 +871,18 @@ config_data! {
/// (i.e., the folder containing the `Cargo.toml`). This can be overwritten
/// by changing `#rust-analyzer.check.invocationStrategy#`.
///
- /// If `$saved_file` is part of the command, rust-analyzer will pass
- /// the absolute path of the saved file to the provided command. This is
- /// intended to be used with non-Cargo build systems.
- /// Note that `$saved_file` is experimental and may be removed in the future.
+ /// It supports two interpolation syntaxes, both mainly intended to be used with
+ /// [non-Cargo build systems](./non_cargo_based_projects.md):
+ ///
+ /// - If `{saved_file}` is part of the command, rust-analyzer will pass
+ /// the absolute path of the saved file to the provided command.
+ /// (A previous version, `$saved_file`, also works.)
+ /// - If `{label}` is part of the command, rust-analyzer will pass the
+ /// Cargo package ID, which can be used with `cargo check -p`, or a build label from
+ /// `rust-project.json`. If `{label}` is included, rust-analyzer behaves much like
+ /// [`"rust-analyzer.check.workspace": false`](#check.workspace).
+ ///
+ ///
///
/// An example command would be:
///
@@ -2431,6 +2440,8 @@ impl Config {
pub(crate) fn cargo_test_options(&self, source_root: Option<SourceRootId>) -> CargoOptions {
CargoOptions {
+ // Might be nice to allow users to specify test_command = "nextest"
+ subcommand: "test".into(),
target_tuples: self.cargo_target(source_root).clone().into_iter().collect(),
all_targets: false,
no_default_features: *self.cargo_noDefaultFeatures(source_root),
@@ -2464,9 +2475,9 @@ impl Config {
},
}
}
- Some(_) | None => FlycheckConfig::CargoCommand {
- command: self.check_command(source_root).clone(),
- options: CargoOptions {
+ Some(_) | None => FlycheckConfig::Automatic {
+ cargo_options: CargoOptions {
+ subcommand: self.check_command(source_root).clone(),
target_tuples: self
.check_targets(source_root)
.clone()
@@ -4171,8 +4182,8 @@ mod tests {
assert_eq!(config.cargo_targetDir(None), &None);
assert!(matches!(
config.flycheck(None),
- FlycheckConfig::CargoCommand {
- options: CargoOptions { target_dir_config: TargetDirectoryConfig::None, .. },
+ FlycheckConfig::Automatic {
+ cargo_options: CargoOptions { target_dir_config: TargetDirectoryConfig::None, .. },
..
}
));
@@ -4195,8 +4206,8 @@ mod tests {
Utf8PathBuf::from(std::env::var("CARGO_TARGET_DIR").unwrap_or("target".to_owned()));
assert!(matches!(
config.flycheck(None),
- FlycheckConfig::CargoCommand {
- options: CargoOptions { target_dir_config, .. },
+ FlycheckConfig::Automatic {
+ cargo_options: CargoOptions { target_dir_config, .. },
..
} if target_dir_config.target_dir(Some(&ws_target_dir)).map(Cow::into_owned)
== Some(ws_target_dir.join("rust-analyzer"))
@@ -4221,8 +4232,8 @@ mod tests {
);
assert!(matches!(
config.flycheck(None),
- FlycheckConfig::CargoCommand {
- options: CargoOptions { target_dir_config, .. },
+ FlycheckConfig::Automatic {
+ cargo_options: CargoOptions { target_dir_config, .. },
..
} if target_dir_config.target_dir(None).map(Cow::into_owned)
== Some(Utf8PathBuf::from("other_folder"))
diff --git a/crates/rust-analyzer/src/diagnostics.rs b/crates/rust-analyzer/src/diagnostics.rs
index 4a247800af..712960f13d 100644
--- a/crates/rust-analyzer/src/diagnostics.rs
+++ b/crates/rust-analyzer/src/diagnostics.rs
@@ -3,7 +3,6 @@ pub(crate) mod flycheck_to_proto;
use std::mem;
-use cargo_metadata::PackageId;
use ide::FileId;
use ide_db::{FxHashMap, base_db::DbPanicContext};
use itertools::Itertools;
@@ -12,10 +11,13 @@ use smallvec::SmallVec;
use stdx::iter_eq_by;
use triomphe::Arc;
-use crate::{global_state::GlobalStateSnapshot, lsp, lsp_ext, main_loop::DiagnosticsTaskKind};
+use crate::{
+ flycheck::PackageSpecifier, global_state::GlobalStateSnapshot, lsp, lsp_ext,
+ main_loop::DiagnosticsTaskKind,
+};
pub(crate) type CheckFixes =
- Arc<Vec<FxHashMap<Option<Arc<PackageId>>, FxHashMap<FileId, Vec<Fix>>>>>;
+ Arc<Vec<FxHashMap<Option<PackageSpecifier>, FxHashMap<FileId, Vec<Fix>>>>>;
#[derive(Debug, Default, Clone)]
pub struct DiagnosticsMapConfig {
@@ -29,7 +31,7 @@ pub(crate) type DiagnosticsGeneration = usize;
#[derive(Debug, Clone, Default)]
pub(crate) struct WorkspaceFlycheckDiagnostic {
- pub(crate) per_package: FxHashMap<Option<Arc<PackageId>>, PackageFlycheckDiagnostic>,
+ pub(crate) per_package: FxHashMap<Option<PackageSpecifier>, PackageFlycheckDiagnostic>,
}
#[derive(Debug, Clone)]
@@ -85,7 +87,7 @@ impl DiagnosticCollection {
pub(crate) fn clear_check_for_package(
&mut self,
flycheck_id: usize,
- package_id: Arc<PackageId>,
+ package_id: PackageSpecifier,
) {
let Some(check) = self.check.get_mut(flycheck_id) else {
return;
@@ -124,7 +126,7 @@ impl DiagnosticCollection {
pub(crate) fn clear_check_older_than_for_package(
&mut self,
flycheck_id: usize,
- package_id: Arc<PackageId>,
+ package_id: PackageSpecifier,
generation: DiagnosticsGeneration,
) {
let Some(check) = self.check.get_mut(flycheck_id) else {
@@ -154,7 +156,7 @@ impl DiagnosticCollection {
&mut self,
flycheck_id: usize,
generation: DiagnosticsGeneration,
- package_id: &Option<Arc<PackageId>>,
+ package_id: &Option<PackageSpecifier>,
file_id: FileId,
diagnostic: lsp_types::Diagnostic,
fix: Option<Box<Fix>>,
diff --git a/crates/rust-analyzer/src/flycheck.rs b/crates/rust-analyzer/src/flycheck.rs
index b062641691..512c231990 100644
--- a/crates/rust-analyzer/src/flycheck.rs
+++ b/crates/rust-analyzer/src/flycheck.rs
@@ -14,6 +14,7 @@ use ide_db::FxHashSet;
use itertools::Itertools;
use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf};
use project_model::TargetDirectoryConfig;
+use project_model::project_json;
use rustc_hash::FxHashMap;
use serde::Deserialize as _;
use serde_derive::Deserialize;
@@ -21,6 +22,7 @@ use serde_derive::Deserialize;
pub(crate) use cargo_metadata::diagnostic::{
Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
};
+use toolchain::DISPLAY_COMMAND_IGNORE_ENVS;
use toolchain::Tool;
use triomphe::Arc;
@@ -36,8 +38,11 @@ pub(crate) enum InvocationStrategy {
PerWorkspace,
}
+/// Data needed to construct a `cargo` command invocation, e.g. for flycheck or running a test.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CargoOptions {
+ /// The cargo subcommand to run, e.g. "check" or "clippy"
+ pub(crate) subcommand: String,
pub(crate) target_tuples: Vec<String>,
pub(crate) all_targets: bool,
pub(crate) set_test: bool,
@@ -89,13 +94,36 @@ impl CargoOptions {
}
}
+/// The flycheck config from a rust-project.json file or discoverConfig JSON output.
+#[derive(Debug, Default)]
+pub(crate) struct FlycheckConfigJson {
+ /// The template with [project_json::RunnableKind::Flycheck]
+ pub single_template: Option<project_json::Runnable>,
+}
+
+impl FlycheckConfigJson {
+ pub(crate) fn any_configured(&self) -> bool {
+ // self.workspace_template.is_some() ||
+ self.single_template.is_some()
+ }
+}
+
+/// The flycheck config from rust-analyzer's own configuration.
+///
+/// We rely on this when rust-project.json does not specify a flycheck runnable
+///
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum FlycheckConfig {
- CargoCommand {
- command: String,
- options: CargoOptions,
+ /// Automatically use rust-project.json's flycheck runnable or just use cargo (the common case)
+ ///
+ /// We can't have a variant for ProjectJson because that is configured on the fly during
+ /// discoverConfig. We only know what we can read at config time.
+ Automatic {
+ /// If we do use cargo, how to build the check command
+ cargo_options: CargoOptions,
ansi_color_output: bool,
},
+ /// check_overrideCommand. This overrides both cargo and rust-project.json's flycheck runnable.
CustomCommand {
command: String,
args: Vec<String>,
@@ -107,7 +135,7 @@ pub(crate) enum FlycheckConfig {
impl FlycheckConfig {
pub(crate) fn invocation_strategy(&self) -> InvocationStrategy {
match self {
- FlycheckConfig::CargoCommand { .. } => InvocationStrategy::PerWorkspace,
+ FlycheckConfig::Automatic { .. } => InvocationStrategy::PerWorkspace,
FlycheckConfig::CustomCommand { invocation_strategy, .. } => {
invocation_strategy.clone()
}
@@ -118,7 +146,9 @@ impl FlycheckConfig {
impl fmt::Display for FlycheckConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
+ FlycheckConfig::Automatic { cargo_options, .. } => {
+ write!(f, "cargo {}", cargo_options.subcommand)
+ }
FlycheckConfig::CustomCommand { command, args, .. } => {
// Don't show `my_custom_check --foo $saved_file` literally to the user, as it
// looks like we've forgotten to substitute $saved_file.
@@ -128,7 +158,7 @@ impl fmt::Display for FlycheckConfig {
// in the IDE (e.g. in the VS Code status bar).
let display_args = args
.iter()
- .map(|arg| if arg == SAVED_FILE_PLACEHOLDER { "..." } else { arg })
+ .map(|arg| if arg == SAVED_FILE_PLACEHOLDER_DOLLAR { "..." } else { arg })
.collect::<Vec<_>>();
write!(f, "{command} {}", display_args.join(" "))
@@ -156,6 +186,7 @@ impl FlycheckHandle {
generation: Arc<AtomicUsize>,
sender: Sender<FlycheckMessage>,
config: FlycheckConfig,
+ config_json: FlycheckConfigJson,
sysroot_root: Option<AbsPathBuf>,
workspace_root: AbsPathBuf,
manifest_path: Option<AbsPathBuf>,
@@ -166,6 +197,7 @@ impl FlycheckHandle {
generation.load(Ordering::Relaxed),
sender,
config,
+ config_json,
sysroot_root,
workspace_root,
manifest_path,
@@ -195,16 +227,17 @@ impl FlycheckHandle {
/// Schedule a re-start of the cargo check worker to do a package wide check.
pub(crate) fn restart_for_package(
&self,
- package: Arc<PackageId>,
+ package: PackageSpecifier,
target: Option<Target>,
- workspace_deps: Option<FxHashSet<Arc<PackageId>>>,
+ workspace_deps: Option<FxHashSet<PackageSpecifier>>,
+ saved_file: Option<AbsPathBuf>,
) {
let generation = self.generation.fetch_add(1, Ordering::Relaxed) + 1;
self.sender
.send(StateChange::Restart {
generation,
scope: FlycheckScope::Package { package, workspace_deps },
- saved_file: None,
+ saved_file,
target,
})
.unwrap();
@@ -233,7 +266,7 @@ pub(crate) enum ClearDiagnosticsKind {
#[derive(Debug)]
pub(crate) enum ClearScope {
Workspace,
- Package(Arc<PackageId>),
+ Package(PackageSpecifier),
}
pub(crate) enum FlycheckMessage {
@@ -243,7 +276,7 @@ pub(crate) enum FlycheckMessage {
generation: DiagnosticsGeneration,
workspace_root: Arc<AbsPathBuf>,
diagnostic: Diagnostic,
- package_id: Option<Arc<PackageId>>,
+ package_id: Option<PackageSpecifier>,
},
/// Request clearing all outdated diagnostics.
@@ -286,16 +319,56 @@ impl fmt::Debug for FlycheckMessage {
#[derive(Debug)]
pub(crate) enum Progress {
- DidStart,
+ DidStart {
+ /// The user sees this in VSCode, etc. May be a shortened version of the command we actually
+ /// executed, otherwise it is way too long.
+ user_facing_command: String,
+ },
DidCheckCrate(String),
DidFinish(io::Result<()>),
DidCancel,
DidFailToRestart(String),
}
+#[derive(Debug, Clone)]
enum FlycheckScope {
Workspace,
- Package { package: Arc<PackageId>, workspace_deps: Option<FxHashSet<Arc<PackageId>>> },
+ Package {
+ // Either a cargo package or a $label in rust-project.check.overrideCommand
+ package: PackageSpecifier,
+ workspace_deps: Option<FxHashSet<PackageSpecifier>>,
+ },
+}
+
+#[derive(Debug, Hash, PartialEq, Eq, Clone)]
+pub(crate) enum PackageSpecifier {
+ Cargo {
+ /// The one in Cargo.toml, assumed to work with `cargo check -p {}` etc
+ package_id: Arc<PackageId>,
+ },
+ BuildInfo {
+ /// If a `build` field is present in rust-project.json, its label field
+ label: String,
+ },
+}
+
+impl PackageSpecifier {
+ pub(crate) fn as_str(&self) -> &str {
+ match self {
+ Self::Cargo { package_id } => &package_id.repr,
+ Self::BuildInfo { label } => label,
+ }
+ }
+}
+
+#[derive(Debug)]
+enum FlycheckCommandOrigin {
+ /// Regular cargo invocation
+ Cargo,
+ /// Configured via check_overrideCommand
+ CheckOverrideCommand,
+ /// From a runnable with [project_json::RunnableKind::Flycheck]
+ ProjectJsonRunnable,
}
enum StateChange {
@@ -316,6 +389,8 @@ struct FlycheckActor {
generation: DiagnosticsGeneration,
sender: Sender<FlycheckMessage>,
config: FlycheckConfig,
+ config_json: FlycheckConfigJson,
+
manifest_path: Option<AbsPathBuf>,
ws_target_dir: Option<Utf8PathBuf>,
/// Either the workspace root of the workspace we are flychecking,
@@ -331,7 +406,7 @@ struct FlycheckActor {
command_handle: Option<CommandHandle<CargoCheckMessage>>,
/// The receiver side of the channel mentioned above.
command_receiver: Option<Receiver<CargoCheckMessage>>,
- diagnostics_cleared_for: FxHashSet<Arc<PackageId>>,
+ diagnostics_cleared_for: FxHashSet<PackageSpecifier>,
diagnostics_received: DiagnosticsReceived,
}
@@ -348,7 +423,66 @@ enum Event {
CheckEvent(Option<CargoCheckMessage>),
}
-pub(crate) const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
+/// This is stable behaviour. Don't change.
+const SAVED_FILE_PLACEHOLDER_DOLLAR: &str = "$saved_file";
+const LABEL_INLINE: &str = "{label}";
+const SAVED_FILE_INLINE: &str = "{saved_file}";
+
+struct Substitutions<'a> {
+ label: Option<&'a str>,
+ saved_file: Option<&'a str>,
+}
+
+impl<'a> Substitutions<'a> {
+ /// If you have a runnable, and it has {label} in it somewhere, treat it as a template that
+ /// may be unsatisfied if you do not provide a label to substitute into it. Returns None in
+ /// that situation. Otherwise performs the requested substitutions.
+ ///
+ /// Same for {saved_file}.
+ ///
+ #[allow(clippy::disallowed_types)] /* generic parameter allows for FxHashMap */
+ fn substitute<H>(
+ self,
+ template: &project_json::Runnable,
+ extra_env: &std::collections::HashMap<String, Option<String>, H>,
+ ) -> Option<Command> {
+ let mut cmd = toolchain::command(&template.program, &template.cwd, extra_env);
+ for arg in &template.args {
+ if let Some(ix) = arg.find(LABEL_INLINE) {
+ if let Some(label) = self.label {
+ let mut arg = arg.to_string();
+ arg.replace_range(ix..ix + LABEL_INLINE.len(), label);
+ cmd.arg(arg);
+ continue;
+ } else {
+ return None;
+ }
+ }
+ if let Some(ix) = arg.find(SAVED_FILE_INLINE) {
+ if let Some(saved_file) = self.saved_file {
+ let mut arg = arg.to_string();
+ arg.replace_range(ix..ix + SAVED_FILE_INLINE.len(), saved_file);
+ cmd.arg(arg);
+ continue;
+ } else {
+ return None;
+ }
+ }
+ // Legacy syntax: full argument match
+ if arg == SAVED_FILE_PLACEHOLDER_DOLLAR {
+ if let Some(saved_file) = self.saved_file {
+ cmd.arg(saved_file);
+ continue;
+ } else {
+ return None;
+ }
+ }
+ cmd.arg(arg);
+ }
+ cmd.current_dir(&template.cwd);
+ Some(cmd)
+ }
+}
impl FlycheckActor {
fn new(
@@ -356,6 +490,7 @@ impl FlycheckActor {
generation: DiagnosticsGeneration,
sender: Sender<FlycheckMessage>,
config: FlycheckConfig,
+ config_json: FlycheckConfigJson,
sysroot_root: Option<AbsPathBuf>,
workspace_root: AbsPathBuf,
manifest_path: Option<AbsPathBuf>,
@@ -367,6 +502,7 @@ impl FlycheckActor {
generation,
sender,
config,
+ config_json,
sysroot_root,
root: Arc::new(workspace_root),
scope: FlycheckScope::Workspace,
@@ -418,27 +554,39 @@ impl FlycheckActor {
}
let command = self.check_command(&scope, saved_file.as_deref(), target);
- self.scope = scope;
+ self.scope = scope.clone();
self.generation = generation;
- let Some(command) = command else {
+ let Some((command, origin)) = command else {
+ tracing::debug!(?scope, "failed to build flycheck command");
continue;
};
- let formatted_command = format!("{command:?}");
+ let debug_command = format!("{command:?}");
+ let user_facing_command = match origin {
+ // Don't show all the --format=json-with-blah-blah args, just the simple
+ // version
+ FlycheckCommandOrigin::Cargo => self.config.to_string(),
+ // show them the full command but pretty printed. advanced user
+ FlycheckCommandOrigin::ProjectJsonRunnable
+ | FlycheckCommandOrigin::CheckOverrideCommand => display_command(
+ &command,
+ Some(std::path::Path::new(self.root.as_path())),
+ ),
+ };
- tracing::debug!(?command, "will restart flycheck");
+ tracing::debug!(?origin, ?command, "will restart flycheck");
let (sender, receiver) = unbounded();
match CommandHandle::spawn(
command,
CargoCheckParser,
sender,
match &self.config {
- FlycheckConfig::CargoCommand { options, .. } => {
+ FlycheckConfig::Automatic { cargo_options, .. } => {
let ws_target_dir =
self.ws_target_dir.as_ref().map(Utf8PathBuf::as_path);
let target_dir =
- options.target_dir_config.target_dir(ws_target_dir);
+ cargo_options.target_dir_config.target_dir(ws_target_dir);
// If `"rust-analyzer.cargo.targetDir": null`, we should use
// workspace's target dir instead of hard-coded fallback.
@@ -464,14 +612,14 @@ impl FlycheckActor {
},
) {
Ok(command_handle) => {
- tracing::debug!(command = formatted_command, "did restart flycheck");
+ tracing::debug!(?origin, command = %debug_command, "did restart flycheck");
self.command_handle = Some(command_handle);
self.command_receiver = Some(receiver);
- self.report_progress(Progress::DidStart);
+ self.report_progress(Progress::DidStart { user_facing_command });
}
Err(error) => {
self.report_progress(Progress::DidFailToRestart(format!(
- "Failed to run the following command: {formatted_command} error={error}"
+ "Failed to run the following command: {debug_command} origin={origin:?} error={error}"
)));
}
}
@@ -564,7 +712,10 @@ impl FlycheckActor {
msg.target.kind.iter().format_with(", ", |kind, f| f(&kind)),
)));
let package_id = Arc::new(msg.package_id);
- if self.diagnostics_cleared_for.insert(package_id.clone()) {
+ if self
+ .diagnostics_cleared_for
+ .insert(PackageSpecifier::Cargo { package_id: package_id.clone() })
+ {
tracing::trace!(
flycheck_id = self.id,
package_id = package_id.repr,
@@ -572,7 +723,9 @@ impl FlycheckActor {
);
self.send(FlycheckMessage::ClearDiagnostics {
id: self.id,
- kind: ClearDiagnosticsKind::All(ClearScope::Package(package_id)),
+ kind: ClearDiagnosticsKind::All(ClearScope::Package(
+ PackageSpecifier::Cargo { package_id },
+ )),
});
}
}
@@ -580,7 +733,7 @@ impl FlycheckActor {
tracing::trace!(
flycheck_id = self.id,
message = diagnostic.message,
- package_id = package_id.as_ref().map(|it| &it.repr),
+ package_id = package_id.as_ref().map(|it| it.as_str()),
"diagnostic received"
);
if self.diagnostics_received == DiagnosticsReceived::No {
@@ -590,7 +743,7 @@ impl FlycheckActor {
if self.diagnostics_cleared_for.insert(package_id.clone()) {
tracing::trace!(
flycheck_id = self.id,
- package_id = package_id.repr,
+ package_id = package_id.as_str(),
"clearing diagnostics"
);
self.send(FlycheckMessage::ClearDiagnostics {
@@ -642,6 +795,29 @@ impl FlycheckActor {
self.diagnostics_received = DiagnosticsReceived::No;
}
+ fn explicit_check_command(
+ &self,
+ scope: &FlycheckScope,
+ saved_file: Option<&AbsPath>,
+ ) -> Option<Command> {
+ let label = match scope {
+ // We could add a runnable like "RunnableKind::FlycheckWorkspace". But generally
+ // if you're not running cargo, it's because your workspace is too big to check
+ // all at once. You can always use `check_overrideCommand` with no {label}.
+ FlycheckScope::Workspace => return None,
+ FlycheckScope::Package { package: PackageSpecifier::BuildInfo { label }, .. } => {
+ label.as_str()
+ }
+ FlycheckScope::Package {
+ package: PackageSpecifier::Cargo { package_id: label },
+ ..
+ } => &label.repr,
+ };
+ let template = self.config_json.single_template.as_ref()?;
+ let subs = Substitutions { label: Some(label), saved_file: saved_file.map(|x| x.as_str()) };
+ subs.substitute(template, &FxHashMap::default())
+ }
+
/// Construct a `Command` object for checking the user's code. If the user
/// has specified a custom command with placeholders that we cannot fill,
/// return None.
@@ -650,23 +826,49 @@ impl FlycheckActor {
scope: &FlycheckScope,
saved_file: Option<&AbsPath>,
target: Option<Target>,
- ) -> Option<Command> {
+ ) -> Option<(Command, FlycheckCommandOrigin)> {
match &self.config {
- FlycheckConfig::CargoCommand { command, options, ansi_color_output } => {
+ FlycheckConfig::Automatic { cargo_options, ansi_color_output } => {
+ // Only use the rust-project.json's flycheck config when no check_overrideCommand
+ // is configured. In the FlycheckConcig::CustomCommand branch we will still do
+ // label substitution, but on the overrideCommand instead.
+ //
+ // There needs to be SOME way to override what your discoverConfig tool says,
+ // because to change the flycheck runnable there you may have to literally
+ // recompile the tool.
+ if self.config_json.any_configured() {
+ // Completely handle according to rust-project.json.
+ // We don't consider this to be "using cargo" so we will not apply any of the
+ // CargoOptions to the command.
+ let cmd = self.explicit_check_command(scope, saved_file)?;
+ return Some((cmd, FlycheckCommandOrigin::ProjectJsonRunnable));
+ }
+
let mut cmd =
- toolchain::command(Tool::Cargo.path(), &*self.root, &options.extra_env);
+ toolchain::command(Tool::Cargo.path(), &*self.root, &cargo_options.extra_env);
if let Some(sysroot_root) = &self.sysroot_root
- && !options.extra_env.contains_key("RUSTUP_TOOLCHAIN")
+ && !cargo_options.extra_env.contains_key("RUSTUP_TOOLCHAIN")
&& std::env::var_os("RUSTUP_TOOLCHAIN").is_none()
{
cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
}
cmd.env("CARGO_LOG", "cargo::core::compiler::fingerprint=info");
- cmd.arg(command);
+ cmd.arg(&cargo_options.subcommand);
match scope {
FlycheckScope::Workspace => cmd.arg("--workspace"),
- FlycheckScope::Package { package, .. } => cmd.arg("-p").arg(&package.repr),
+ FlycheckScope::Package {
+ package: PackageSpecifier::Cargo { package_id },
+ ..
+ } => cmd.arg("-p").arg(&package_id.repr),
+ FlycheckScope::Package {
+ package: PackageSpecifier::BuildInfo { .. }, ..
+ } => {
+ // No way to flycheck this single package. All we have is a build label.
+ // There's no way to really say whether this build label happens to be
+ // a cargo canonical name, so we won't try.
+ return None;
+ }
};
if let Some(tgt) = target {
@@ -695,12 +897,12 @@ impl FlycheckActor {
cmd.arg("--keep-going");
- options.apply_on_command(
+ cargo_options.apply_on_command(
&mut cmd,
self.ws_target_dir.as_ref().map(Utf8PathBuf::as_path),
);
- cmd.args(&options.extra_args);
- Some(cmd)
+ cmd.args(&cargo_options.extra_args);
+ Some((cmd, FlycheckCommandOrigin::Cargo))
}
FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => {
let root = match invocation_strategy {
@@ -710,31 +912,25 @@ impl FlycheckActor {
&*self.root
}
};
- let mut cmd = toolchain::command(command, root, extra_env);
-
- // If the custom command has a $saved_file placeholder, and
- // we're saving a file, replace the placeholder in the arguments.
- if let Some(saved_file) = saved_file {
- for arg in args {
- if arg == SAVED_FILE_PLACEHOLDER {
- cmd.arg(saved_file);
- } else {
- cmd.arg(arg);
- }
- }
- } else {
- for arg in args {
- if arg == SAVED_FILE_PLACEHOLDER {
- // The custom command has a $saved_file placeholder,
- // but we had an IDE event that wasn't a file save. Do nothing.
- return None;
- }
+ let runnable = project_json::Runnable {
+ program: command.clone(),
+ cwd: Utf8Path::to_owned(root.as_ref()),
+ args: args.clone(),
+ kind: project_json::RunnableKind::Flycheck,
+ };
- cmd.arg(arg);
- }
- }
+ let label = match scope {
+ FlycheckScope::Workspace => None,
+ // We support substituting both build labels (e.g. buck, bazel) and cargo package ids.
+ // With cargo package ids, you get `cargo check -p path+file:///path/to/rust-analyzer/crates/hir#0.0.0`.
+ // That does work!
+ FlycheckScope::Package { package, .. } => Some(package.as_str()),
+ };
- Some(cmd)
+ let subs = Substitutions { label, saved_file: saved_file.map(|x| x.as_str()) };
+ let cmd = subs.substitute(&runnable, extra_env)?;
+
+ Some((cmd, FlycheckCommandOrigin::CheckOverrideCommand))
}
}
}
@@ -748,7 +944,7 @@ impl FlycheckActor {
#[allow(clippy::large_enum_variant)]
enum CargoCheckMessage {
CompilerArtifact(cargo_metadata::Artifact),
- Diagnostic { diagnostic: Diagnostic, package_id: Option<Arc<PackageId>> },
+ Diagnostic { diagnostic: Diagnostic, package_id: Option<PackageSpecifier> },
}
struct CargoCheckParser;
@@ -767,7 +963,9 @@ impl JsonLinesParser<CargoCheckMessage> for CargoCheckParser {
cargo_metadata::Message::CompilerMessage(msg) => {
Some(CargoCheckMessage::Diagnostic {
diagnostic: msg.message,
- package_id: Some(Arc::new(msg.package_id)),
+ package_id: Some(PackageSpecifier::Cargo {
+ package_id: Arc::new(msg.package_id),
+ }),
})
}
_ => None,
@@ -794,3 +992,181 @@ enum JsonMessage {
Cargo(cargo_metadata::Message),
Rustc(Diagnostic),
}
+
+/// Not good enough to execute in a shell, but good enough to show the user without all the noisy
+/// quotes
+///
+/// Pass implicit_cwd if there is one regarded as the obvious by the user, so we can skip showing it.
+/// Compactness is the aim of the game, the output typically gets truncated quite a lot.
+fn display_command(c: &Command, implicit_cwd: Option<&std::path::Path>) -> String {
+ let mut o = String::new();
+ use std::fmt::Write;
+ let lossy = std::ffi::OsStr::to_string_lossy;
+ if let Some(dir) = c.get_current_dir() {
+ if Some(dir) == implicit_cwd.map(std::path::Path::new) {
+ // pass
+ } else if dir.to_string_lossy().contains(" ") {
+ write!(o, "cd {:?} && ", dir).unwrap();
+ } else {
+ write!(o, "cd {} && ", dir.display()).unwrap();
+ }
+ }
+ for (env, val) in c.get_envs() {
+ let (env, val) = (lossy(env), val.map(lossy).unwrap_or(std::borrow::Cow::Borrowed("")));
+ if DISPLAY_COMMAND_IGNORE_ENVS.contains(&env.as_ref()) {
+ continue;
+ }
+ if env.contains(" ") {
+ write!(o, "\"{}={}\" ", env, val).unwrap();
+ } else if val.contains(" ") {
+ write!(o, "{}=\"{}\" ", env, val).unwrap();
+ } else {
+ write!(o, "{}={} ", env, val).unwrap();
+ }
+ }
+ let prog = lossy(c.get_program());
+ if prog.contains(" ") {
+ write!(o, "{:?}", prog).unwrap();
+ } else {
+ write!(o, "{}", prog).unwrap();
+ }
+ for arg in c.get_args() {
+ let arg = lossy(arg);
+ if arg.contains(" ") {
+ write!(o, " \"{}\"", arg).unwrap();
+ } else {
+ write!(o, " {}", arg).unwrap();
+ }
+ }
+ o
+}
+
+#[cfg(test)]
+mod tests {
+ use ide_db::FxHashMap;
+ use itertools::Itertools;
+ use paths::Utf8Path;
+ use project_model::project_json;
+
+ use crate::flycheck::Substitutions;
+ use crate::flycheck::display_command;
+
+ #[test]
+ fn test_substitutions() {
+ let label = ":label";
+ let saved_file = "file.rs";
+
+ // Runnable says it needs both; you need both.
+ assert_eq!(test_substitute(None, None, "{label} {saved_file}").as_deref(), None);
+ assert_eq!(test_substitute(Some(label), None, "{label} {saved_file}").as_deref(), None);
+ assert_eq!(
+ test_substitute(None, Some(saved_file), "{label} {saved_file}").as_deref(),
+ None
+ );
+ assert_eq!(
+ test_substitute(Some(label), Some(saved_file), "{label} {saved_file}").as_deref(),
+ Some("build :label file.rs")
+ );
+
+ // Only need label? only need label.
+ assert_eq!(test_substitute(None, None, "{label}").as_deref(), None);
+ assert_eq!(test_substitute(Some(label), None, "{label}").as_deref(), Some("build :label"),);
+ assert_eq!(test_substitute(None, Some(saved_file), "{label}").as_deref(), None,);
+ assert_eq!(
+ test_substitute(Some(label), Some(saved_file), "{label}").as_deref(),
+ Some("build :label"),
+ );
+
+ // Only need saved_file
+ assert_eq!(test_substitute(None, None, "{saved_file}").as_deref(), None);
+ assert_eq!(test_substitute(Some(label), None, "{saved_file}").as_deref(), None);
+ assert_eq!(
+ test_substitute(None, Some(saved_file), "{saved_file}").as_deref(),
+ Some("build file.rs")
+ );
+ assert_eq!(
+ test_substitute(Some(label), Some(saved_file), "{saved_file}").as_deref(),
+ Some("build file.rs")
+ );
+
+ // Need neither
+ assert_eq!(test_substitute(None, None, "xxx").as_deref(), Some("build xxx"));
+ assert_eq!(test_substitute(Some(label), None, "xxx").as_deref(), Some("build xxx"));
+ assert_eq!(test_substitute(None, Some(saved_file), "xxx").as_deref(), Some("build xxx"));
+ assert_eq!(
+ test_substitute(Some(label), Some(saved_file), "xxx").as_deref(),
+ Some("build xxx")
+ );
+
+ // {label} mid-argument substitution
+ assert_eq!(
+ test_substitute(Some(label), None, "--label={label}").as_deref(),
+ Some("build --label=:label")
+ );
+
+ // {saved_file} mid-argument substitution
+ assert_eq!(
+ test_substitute(None, Some(saved_file), "--saved={saved_file}").as_deref(),
+ Some("build --saved=file.rs")
+ );
+
+ // $saved_file legacy support (no mid-argument substitution, we never supported that)
+ assert_eq!(
+ test_substitute(None, Some(saved_file), "$saved_file").as_deref(),
+ Some("build file.rs")
+ );
+
+ fn test_substitute(
+ label: Option<&str>,
+ saved_file: Option<&str>,
+ args: &str,
+ ) -> Option<String> {
+ Substitutions { label, saved_file }
+ .substitute(
+ &project_json::Runnable {
+ program: "build".to_owned(),
+ args: Vec::from_iter(args.split_whitespace().map(ToOwned::to_owned)),
+ cwd: Utf8Path::new("/path").to_owned(),
+ kind: project_json::RunnableKind::Flycheck,
+ },
+ &FxHashMap::default(),
+ )
+ .map(|command| {
+ command.get_args().map(|x| x.to_string_lossy()).collect_vec().join(" ")
+ })
+ .map(|args| format!("build {}", args))
+ }
+ }
+
+ #[test]
+ fn test_display_command() {
+ use std::path::Path;
+ let workdir = Path::new("workdir");
+ let mut cmd = toolchain::command("command", workdir, &FxHashMap::default());
+ assert_eq!(display_command(cmd.arg("--arg"), Some(workdir)), "command --arg");
+ assert_eq!(
+ display_command(cmd.arg("spaced arg"), Some(workdir)),
+ "command --arg \"spaced arg\""
+ );
+ assert_eq!(
+ display_command(cmd.env("ENVIRON", "yeah"), Some(workdir)),
+ "ENVIRON=yeah command --arg \"spaced arg\""
+ );
+ assert_eq!(
+ display_command(cmd.env("OTHER", "spaced env"), Some(workdir)),
+ "ENVIRON=yeah OTHER=\"spaced env\" command --arg \"spaced arg\""
+ );
+ assert_eq!(
+ display_command(cmd.current_dir("/tmp"), Some(workdir)),
+ "cd /tmp && ENVIRON=yeah OTHER=\"spaced env\" command --arg \"spaced arg\""
+ );
+ assert_eq!(
+ display_command(cmd.current_dir("/tmp and/thing"), Some(workdir)),
+ "cd \"/tmp and/thing\" && ENVIRON=yeah OTHER=\"spaced env\" command --arg \"spaced arg\""
+ );
+ assert_eq!(
+ display_command(cmd.current_dir("/tmp and/thing"), Some(Path::new("/tmp and/thing"))),
+ "ENVIRON=yeah OTHER=\"spaced env\" command --arg \"spaced arg\""
+ );
+ }
+}
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index 9beab3c0e4..39b4aaa647 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -9,7 +9,6 @@ use std::{
time::{Duration, Instant},
};
-use cargo_metadata::PackageId;
use crossbeam_channel::{Receiver, Sender, unbounded};
use hir::ChangeWithProcMacros;
use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId};
@@ -36,7 +35,7 @@ use crate::{
config::{Config, ConfigChange, ConfigErrors, RatomlFileKind},
diagnostics::{CheckFixes, DiagnosticCollection},
discover,
- flycheck::{FlycheckHandle, FlycheckMessage},
+ flycheck::{FlycheckHandle, FlycheckMessage, PackageSpecifier},
line_index::{LineEndings, LineIndex},
lsp::{from_proto, to_proto::url_from_abs_path},
lsp_ext,
@@ -113,6 +112,7 @@ pub(crate) struct GlobalState {
pub(crate) flycheck_sender: Sender<FlycheckMessage>,
pub(crate) flycheck_receiver: Receiver<FlycheckMessage>,
pub(crate) last_flycheck_error: Option<String>,
+ pub(crate) flycheck_formatted_commands: Vec<String>,
// Test explorer
pub(crate) test_run_session: Option<Vec<CargoTestHandle>>,
@@ -289,6 +289,7 @@ impl GlobalState {
flycheck_sender,
flycheck_receiver,
last_flycheck_error: None,
+ flycheck_formatted_commands: vec![],
test_run_session: None,
test_run_sender,
@@ -825,7 +826,7 @@ impl GlobalStateSnapshot {
let Some(krate) = project.crate_by_root(path) else {
continue;
};
- let Some(build) = krate.build else {
+ let Some(build) = krate.build.clone() else {
continue;
};
@@ -833,6 +834,7 @@ impl GlobalStateSnapshot {
label: build.label,
target_kind: build.target_kind,
shell_runnables: project.runnables().to_owned(),
+ project_root: project.project_root().to_owned(),
}));
}
ProjectWorkspaceKind::DetachedFile { .. } => {}
@@ -844,23 +846,43 @@ impl GlobalStateSnapshot {
pub(crate) fn all_workspace_dependencies_for_package(
&self,
- package: &Arc<PackageId>,
- ) -> Option<FxHashSet<Arc<PackageId>>> {
- for workspace in self.workspaces.iter() {
- match &workspace.kind {
- ProjectWorkspaceKind::Cargo { cargo, .. }
- | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _, _)), .. } => {
- let package = cargo.packages().find(|p| cargo[*p].id == *package)?;
-
- return cargo[package]
- .all_member_deps
- .as_ref()
- .map(|deps| deps.iter().map(|dep| cargo[*dep].id.clone()).collect());
- }
- _ => {}
+ package: &PackageSpecifier,
+ ) -> Option<FxHashSet<PackageSpecifier>> {
+ match package {
+ PackageSpecifier::Cargo { package_id } => {
+ self.workspaces.iter().find_map(|workspace| match &workspace.kind {
+ ProjectWorkspaceKind::Cargo { cargo, .. }
+ | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _, _)), .. } => {
+ let package = cargo.packages().find(|p| cargo[*p].id == *package_id)?;
+
+ cargo[package].all_member_deps.as_ref().map(|deps| {
+ deps.iter()
+ .map(|dep| cargo[*dep].id.clone())
+ .map(|p| PackageSpecifier::Cargo { package_id: p })
+ .collect()
+ })
+ }
+ _ => None,
+ })
+ }
+ PackageSpecifier::BuildInfo { label } => {
+ self.workspaces.iter().find_map(|workspace| match &workspace.kind {
+ ProjectWorkspaceKind::Json(p) => {
+ let krate = p.crate_by_label(label)?;
+ Some(
+ krate
+ .iter_deps()
+ .filter_map(|dep| p[dep].build.as_ref())
+ .map(|build| PackageSpecifier::BuildInfo {
+ label: build.label.clone(),
+ })
+ .collect(),
+ )
+ }
+ _ => None,
+ })
}
}
- None
}
pub(crate) fn file_exists(&self, file_id: FileId) -> bool {
diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs
index 4a6544508f..d956010433 100644
--- a/crates/rust-analyzer/src/handlers/notification.rs
+++ b/crates/rust-analyzer/src/handlers/notification.rs
@@ -18,7 +18,7 @@ use vfs::{AbsPathBuf, ChangeKind, VfsPath};
use crate::{
config::{Config, ConfigChange},
- flycheck::{InvocationStrategy, Target},
+ flycheck::{InvocationStrategy, PackageSpecifier, Target},
global_state::{FetchWorkspaceRequest, GlobalState},
lsp::{from_proto, utils::apply_document_changes},
lsp_ext::{self, RunFlycheckParams},
@@ -328,22 +328,33 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
}
InvocationStrategy::PerWorkspace => {
Box::new(move || {
- let target = TargetSpec::for_file(&world, file_id)?.and_then(|it| {
+ let saved_file = vfs_path.as_path().map(ToOwned::to_owned);
+ let target = TargetSpec::for_file(&world, file_id)?.map(|it| {
let tgt_kind = it.target_kind();
let (tgt_name, root, package) = match it {
- TargetSpec::Cargo(c) => (c.target, c.workspace_root, c.package_id),
- _ => return None,
+ TargetSpec::Cargo(c) => (
+ Some(c.target),
+ c.workspace_root,
+ PackageSpecifier::Cargo { package_id: c.package_id },
+ ),
+ TargetSpec::ProjectJson(p) => (
+ None,
+ p.project_root,
+ PackageSpecifier::BuildInfo { label: p.label.clone() },
+ ),
};
- let tgt = match tgt_kind {
- project_model::TargetKind::Bin => Target::Bin(tgt_name),
- project_model::TargetKind::Example => Target::Example(tgt_name),
- project_model::TargetKind::Test => Target::Test(tgt_name),
- project_model::TargetKind::Bench => Target::Benchmark(tgt_name),
- _ => return Some((None, root, package)),
- };
+ let tgt = tgt_name.and_then(|tgt_name| {
+ Some(match tgt_kind {
+ project_model::TargetKind::Bin => Target::Bin(tgt_name),
+ project_model::TargetKind::Example => Target::Example(tgt_name),
+ project_model::TargetKind::Test => Target::Test(tgt_name),
+ project_model::TargetKind::Bench => Target::Benchmark(tgt_name),
+ _ => return None,
+ })
+ });
- Some((Some(tgt), root, package))
+ (tgt, root, package)
});
tracing::debug!(?target, "flycheck target");
// we have a specific non-library target, attempt to only check that target, nothing
@@ -352,8 +363,10 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
if let Some((target, root, package)) = target {
// trigger a package check if we have a non-library target as that can't affect
// anything else in the workspace OR if we're not allowed to check the workspace as
- // the user opted into package checks then
- let package_check_allowed = target.is_some() || !may_flycheck_workspace;
+ // the user opted into package checks then OR if this is not cargo.
+ let package_check_allowed = target.is_some()
+ || !may_flycheck_workspace
+ || matches!(package, PackageSpecifier::BuildInfo { .. });
if package_check_allowed {
package_workspace_idx =
world.workspaces.iter().position(|ws| match &ws.kind {
@@ -365,7 +378,13 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
cargo: Some((cargo, _, _)),
..
} => *cargo.workspace_root() == root,
- _ => false,
+ project_model::ProjectWorkspaceKind::Json(p) => {
+ *p.project_root() == root
+ }
+ project_model::ProjectWorkspaceKind::DetachedFile {
+ cargo: None,
+ ..
+ } => false,
});
if let Some(idx) = package_workspace_idx {
let workspace_deps =
@@ -374,6 +393,7 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
package,
target,
workspace_deps,
+ saved_file.clone(),
);
}
}
@@ -444,7 +464,6 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
ws_contains_file && !is_pkg_ws
});
- let saved_file = vfs_path.as_path().map(ToOwned::to_owned);
let mut workspace_check_triggered = false;
// Find and trigger corresponding flychecks
'flychecks: for flycheck in world.flycheck.iter() {
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index dd0813c144..62a3b3a17b 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -1179,8 +1179,24 @@ impl GlobalState {
kind: ClearDiagnosticsKind::OlderThan(generation, ClearScope::Package(package_id)),
} => self.diagnostics.clear_check_older_than_for_package(id, package_id, generation),
FlycheckMessage::Progress { id, progress } => {
+ let format_with_id = |user_facing_command: String| {
+ if self.flycheck.len() == 1 {
+ user_facing_command
+ } else {
+ format!("{user_facing_command} (#{})", id + 1)
+ }
+ };
+
+ self.flycheck_formatted_commands
+ .resize_with(self.flycheck.len().max(id + 1), || {
+ format_with_id(self.config.flycheck(None).to_string())
+ });
+
let (state, message) = match progress {
- flycheck::Progress::DidStart => (Progress::Begin, None),
+ flycheck::Progress::DidStart { user_facing_command } => {
+ self.flycheck_formatted_commands[id] = format_with_id(user_facing_command);
+ (Progress::Begin, None)
+ }
flycheck::Progress::DidCheckCrate(target) => (Progress::Report, Some(target)),
flycheck::Progress::DidCancel => {
self.last_flycheck_error = None;
@@ -1200,13 +1216,8 @@ impl GlobalState {
}
};
- // When we're running multiple flychecks, we have to include a disambiguator in
- // the title, or the editor complains. Note that this is a user-facing string.
- let title = if self.flycheck.len() == 1 {
- format!("{}", self.config.flycheck(None))
- } else {
- format!("{} (#{})", self.config.flycheck(None), id + 1)
- };
+ // Clone because we &mut self for report_progress
+ let title = self.flycheck_formatted_commands[id].clone();
self.report_progress(
&title,
state,
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index e3a5ee2219..ccafbd7b30 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -25,7 +25,9 @@ use load_cargo::{ProjectFolders, load_proc_macro};
use lsp_types::FileSystemWatcher;
use paths::Utf8Path;
use proc_macro_api::ProcMacroClient;
-use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
+use project_model::{
+ ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts, project_json,
+};
use stdx::{format_to, thread::ThreadIntent};
use triomphe::Arc;
use vfs::{AbsPath, AbsPathBuf, ChangeKind};
@@ -875,6 +877,7 @@ impl GlobalState {
generation.clone(),
sender.clone(),
config,
+ crate::flycheck::FlycheckConfigJson::default(),
None,
self.config.root_path().clone(),
None,
@@ -894,16 +897,25 @@ impl GlobalState {
cargo: Some((cargo, _, _)),
..
} => (
+ crate::flycheck::FlycheckConfigJson::default(),
cargo.workspace_root(),
Some(cargo.manifest_path()),
Some(cargo.target_directory()),
),
ProjectWorkspaceKind::Json(project) => {
+ let config_json = crate::flycheck::FlycheckConfigJson {
+ single_template: project
+ .runnable_template(project_json::RunnableKind::Flycheck)
+ .cloned(),
+ };
// Enable flychecks for json projects if a custom flycheck command was supplied
// in the workspace configuration.
match config {
+ _ if config_json.any_configured() => {
+ (config_json, project.path(), None, None)
+ }
FlycheckConfig::CustomCommand { .. } => {
- (project.path(), None, None)
+ (config_json, project.path(), None, None)
}
_ => return None,
}
@@ -913,12 +925,13 @@ impl GlobalState {
ws.sysroot.root().map(ToOwned::to_owned),
))
})
- .map(|(id, (root, manifest_path, target_dir), sysroot_root)| {
+ .map(|(id, (config_json, root, manifest_path, target_dir), sysroot_root)| {
FlycheckHandle::spawn(
id,
generation.clone(),
sender.clone(),
config.clone(),
+ config_json,
sysroot_root,
root.to_path_buf(),
manifest_path.map(|it| it.to_path_buf()),
@@ -929,6 +942,7 @@ impl GlobalState {
}
}
.into();
+ self.flycheck_formatted_commands = vec![];
}
}
diff --git a/crates/rust-analyzer/src/target_spec.rs b/crates/rust-analyzer/src/target_spec.rs
index e0f95a7830..b8d9acc02a 100644
--- a/crates/rust-analyzer/src/target_spec.rs
+++ b/crates/rust-analyzer/src/target_spec.rs
@@ -68,6 +68,7 @@ pub(crate) struct ProjectJsonTargetSpec {
pub(crate) label: String,
pub(crate) target_kind: TargetKind,
pub(crate) shell_runnables: Vec<Runnable>,
+ pub(crate) project_root: AbsPathBuf,
}
impl ProjectJsonTargetSpec {
@@ -76,7 +77,16 @@ impl ProjectJsonTargetSpec {
RunnableKind::Bin => {
for runnable in &self.shell_runnables {
if matches!(runnable.kind, project_model::project_json::RunnableKind::Run) {
- return Some(runnable.clone());
+ let mut runnable = runnable.clone();
+
+ let replaced_args: Vec<_> = runnable
+ .args
+ .iter()
+ .map(|arg| arg.replace("{label}", &self.label))
+ .collect();
+ runnable.args = replaced_args;
+
+ return Some(runnable);
}
}
diff --git a/crates/rust-analyzer/src/test_runner.rs b/crates/rust-analyzer/src/test_runner.rs
index 7111a15d02..f0020f9088 100644
--- a/crates/rust-analyzer/src/test_runner.rs
+++ b/crates/rust-analyzer/src/test_runner.rs
@@ -105,7 +105,7 @@ impl CargoTestHandle {
let mut cmd = toolchain::command(Tool::Cargo.path(), root, &options.extra_env);
cmd.env("RUSTC_BOOTSTRAP", "1");
cmd.arg("--color=always");
- cmd.arg("test");
+ cmd.arg(&options.subcommand); // test, usually
cmd.arg("--package");
cmd.arg(&test_target.package);
diff --git a/crates/toolchain/src/lib.rs b/crates/toolchain/src/lib.rs
index 39319886cf..1a17269838 100644
--- a/crates/toolchain/src/lib.rs
+++ b/crates/toolchain/src/lib.rs
@@ -74,6 +74,9 @@ impl Tool {
// Prevent rustup from automatically installing toolchains, see https://github.com/rust-lang/rust-analyzer/issues/20719.
pub const NO_RUSTUP_AUTO_INSTALL_ENV: (&str, &str) = ("RUSTUP_AUTO_INSTALL", "0");
+// These get ignored when displaying what command is running in LSP status messages.
+pub const DISPLAY_COMMAND_IGNORE_ENVS: &[&str] = &[NO_RUSTUP_AUTO_INSTALL_ENV.0];
+
#[allow(clippy::disallowed_types)] /* generic parameter allows for FxHashMap */
pub fn command<H>(
cmd: impl AsRef<OsStr>,
diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md
index 58b6363345..c4124aaae0 100644
--- a/docs/book/src/configuration_generated.md
+++ b/docs/book/src/configuration_generated.md
@@ -323,10 +323,18 @@ each of them, with the working directory being the workspace root
(i.e., the folder containing the `Cargo.toml`). This can be overwritten
by changing `#rust-analyzer.check.invocationStrategy#`.
-If `$saved_file` is part of the command, rust-analyzer will pass
-the absolute path of the saved file to the provided command. This is
-intended to be used with non-Cargo build systems.
-Note that `$saved_file` is experimental and may be removed in the future.
+It supports two interpolation syntaxes, both mainly intended to be used with
+[non-Cargo build systems](./non_cargo_based_projects.md):
+
+- If `{saved_file}` is part of the command, rust-analyzer will pass
+ the absolute path of the saved file to the provided command.
+ (A previous version, `$saved_file`, also works.)
+- If `{label}` is part of the command, rust-analyzer will pass the
+ Cargo package ID, which can be used with `cargo check -p`, or a build label from
+ `rust-project.json`. If `{label}` is included, rust-analyzer behaves much like
+ [`"rust-analyzer.check.workspace": false`](#check.workspace).
+
+
An example command would be:
@@ -1613,8 +1621,8 @@ Default: `null`
Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].
-[`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`.
-`progress_label` is used for the title in progress indicators, whereas `files_to_watch`
+[`DiscoverWorkspaceConfig`] also requires setting `progressLabel` and `filesToWatch`.
+`progressLabel` is used for the title in progress indicators, whereas `filesToWatch`
is used to determine which build system-specific files should be watched in order to
reload rust-analyzer.
@@ -1623,16 +1631,17 @@ Below is an example of a valid configuration:
"rust-analyzer.workspace.discoverConfig": {
"command": [
"rust-project",
- "develop-json"
+ "develop-json",
+ "{arg}"
],
- "progressLabel": "rust-analyzer",
+ "progressLabel": "buck2/rust-project",
"filesToWatch": [
"BUCK"
]
}
```
-## On `DiscoverWorkspaceConfig::command`
+## Workspace Discovery Protocol
**Warning**: This format is provisional and subject to change.
diff --git a/docs/book/src/non_cargo_based_projects.md b/docs/book/src/non_cargo_based_projects.md
index e7df4a5d76..a48b025c7b 100644
--- a/docs/book/src/non_cargo_based_projects.md
+++ b/docs/book/src/non_cargo_based_projects.md
@@ -204,23 +204,40 @@ interface Runnable {
args: string[];
/// The current working directory of the runnable.
cwd: string;
- /// Used to decide what code lens to offer.
+ /// Maps a runnable to a piece of rust-analyzer functionality.
///
- /// `testOne`: This runnable will be used when the user clicks the 'Run Test'
- /// CodeLens above a test.
+ /// - `testOne`: This runnable will be used when the user clicks the 'Run Test'
+ /// CodeLens above a test.
+ /// - `run`: This runnable will be used when the user clicks the 'Run' CodeLens
+ /// above a main function or triggers a run command.
+ /// - `flycheck`: This is run to provide check-on-save diagnostics when the user
+ /// saves a file. It must emit rustc JSON diagnostics that rust-analyzer can
+ /// parse. If this runnable is not specified, we may try to use `cargo check -p`.
+ /// This is only run for a single crate that the user saved a file in. The
+ /// {label} syntax is replaced with `BuildInfo::label`.
+ /// Alternatively, you may use `{saved_file}` and figure out which crate
+ /// to produce diagnostics for based on that.
///
/// The args for testOne can contain two template strings:
/// `{label}` and `{test_id}`. `{label}` will be replaced
- /// with the `Build::label` and `{test_id}` will be replaced
+ /// with the `BuildInfo::label` and `{test_id}` will be replaced
/// with the test name.
- kind: 'testOne' | string;
+ kind: 'testOne' | 'run' | 'flycheck' | string;
}
```
This format is provisional and subject to change. Specifically, the
`roots` setup will be different eventually.
-There are three ways to feed `rust-project.json` to rust-analyzer:
+### Providing a JSON project to rust-analyzer
+
+There are four ways to feed `rust-project.json` to rust-analyzer:
+
+- Use
+ [`"rust-analyzer.workspace.discoverConfig": … }`](./configuration.md#workspace.discoverConfig)
+ to specify a workspace discovery command to generate project descriptions
+ on-the-fly. Please note that the command output is message-oriented and must
+ follow [the discovery protocol](./configuration.md#workspace-discovery-protocol).
- Place `rust-project.json` file at the root of the project, and
rust-analyzer will discover it.
@@ -240,19 +257,86 @@ location or (for inline JSON) relative to `rootUri`.
You can set the `RA_LOG` environment variable to `rust_analyzer=info` to
inspect how rust-analyzer handles config and project loading.
-Note that calls to `cargo check` are disabled when using
-`rust-project.json` by default, so compilation errors and warnings will
-no longer be sent to your LSP client. To enable these compilation errors
-you will need to specify explicitly what command rust-analyzer should
-run to perform the checks using the
-`rust-analyzer.check.overrideCommand` configuration. As an example, the
-following configuration explicitly sets `cargo check` as the `check`
-command.
-
- { "rust-analyzer.check.overrideCommand": ["cargo", "check", "--message-format=json"] }
-
-`check.overrideCommand` requires the command specified to output json
-error messages for rust-analyzer to consume. The `--message-format=json`
-flag does this for `cargo check` so whichever command you use must also
-output errors in this format. See the [Configuration](#_configuration)
-section for more information.
+### Flycheck support
+
+Rust-analyzer has functionality to run an actual build of a crate when the user saves a file, to
+fill in diagnostics it does not implement natively. This is known as "flycheck".
+
+**Flycheck is disabled when using `rust-project.json` unless explicitly configured**, so compilation
+errors and warnings will no longer be sent to your LSP client by default. To enable these
+compilation errors you will need to specify explicitly what command rust-analyzer should run to
+perform the checks. There are two ways to do this:
+
+- `rust-project.json` may contain a `runnables` field. The `flycheck` runnable may be used to
+ configure a check command. See above for documentation.
+
+- Using the [`rust-analyzer.check.overrideCommand`](./configuration.md#check.overrideCommand)
+ configuration. This will also override anything in `rust-project.json`. As an example, the
+ following configuration explicitly sets `cargo check` as the `check` command.
+
+ ```json
+ { "rust-analyzer.check.overrideCommand": ["cargo", "check", "--message-format=json"] }
+ ```
+
+ Note also that this works with cargo projects.
+
+Either option requires the command specified to output JSON error messages for rust-analyzer to
+consume. The `--message-format=json` flag does this for `cargo check` so whichever command you use
+must also output errors in this format.
+
+Either option also supports two syntaxes within each argument:
+
+- `{label}` will be replaced with the `BuildInfo::label` of the crate
+ containing a saved file, if `BuildInfo` is provided. In the case of `check.overrideCommand` being
+ used in a Cargo project, this will be the cargo package ID, which can be used with `cargo check -p`.
+- `{saved_file}` will be replaced with an absolute path to the saved file. This can be queried against a
+ build system to find targets that include the file.
+
+For example:
+
+```json
+{ "rust-analyzer.check.overrideCommand": ["custom_crate_checker", "{label}"] }
+```
+
+If you do use `{label}` or `{saved_file}`, the command will not be run unless the relevant value can
+be substituted.
+
+
+#### Flycheck considerations
+
+##### Diagnostic output on error
+
+A flycheck command using a complex build orchestrator like `"bazel", "build", "{label}"`, even with
+a tweak to return JSON messages, is often insufficient. Such a command will typically succeed if
+there are warnings, but if there are errors, it might "fail to compile" the diagnostics and not
+produce any output. You must build a package in such a way that the build succeeds even if `rustc`
+exits with an error, and prints the JSON build messages in every case.
+
+##### Diagnostics for upstream crates
+
+`cargo check -p` re-prints any errors and warnings in crates higher up in the dependency graph
+than the one requested. We do clear all diagnostics when flychecking, so if you manage to
+replicate this behaviour, diagnostics for crates other than the one being checked will show up in
+the editor. If you do not, then users may be confused that diagnostics are "stuck" or disappear
+entirely when there is a build error in an upstream crate.
+
+##### Compiler options
+
+`cargo check` invokes rustc differently from `cargo build`. It turns off codegen (with `rustc
+--emit=metadata`), which results in lower latency to get to diagnostics. If your build system can
+configure this, it is recommended.
+
+If your build tool can configure rustc for incremental compiles, this is also recommended.
+
+##### Locking and pre-emption
+
+In any good build system, including Cargo, build commands sometimes block each other. Running a
+flycheck will (by default) frequently block you from running other build commands. Generally this is
+undesirable. Users will have to (unintuitively) press save again in the editor to cancel a
+flycheck, so that some other command may proceed.
+
+If your build system has the ability to isolate any rust-analyzer-driven flychecks and prevent lock
+contention, for example a separate build output directory and/or daemon instance, this is
+recommended. Alternatively, consider using a feature if available that can set the priority of
+various build invocations and automatically cancel lower-priority ones when needed. Flychecks should
+be set to a lower priority than general direct build invocations.
diff --git a/editors/code/package.json b/editors/code/package.json
index 2157cbd486..0d91378706 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -1213,7 +1213,7 @@
"title": "Check",
"properties": {
"rust-analyzer.check.overrideCommand": {
- "markdownDescription": "Override the command rust-analyzer uses instead of `cargo check` for\ndiagnostics on save. The command is required to output json and\nshould therefore include `--message-format=json` or a similar option\n(if your client supports the `colorDiagnosticOutput` experimental\ncapability, you can use `--message-format=json-diagnostic-rendered-ansi`).\n\nIf you're changing this because you're using some tool wrapping\nCargo, you might also want to change\n`#rust-analyzer.cargo.buildScripts.overrideCommand#`.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.check.invocationStrategy#`.\n\nIf `$saved_file` is part of the command, rust-analyzer will pass\nthe absolute path of the saved file to the provided command. This is\nintended to be used with non-Cargo build systems.\nNote that `$saved_file` is experimental and may be removed in the future.\n\nAn example command would be:\n\n```bash\ncargo check --workspace --message-format=json --all-targets\n```\n\nNote: The option must be specified as an array of command line arguments, with\nthe first argument being the name of the command to run.",
+ "markdownDescription": "Override the command rust-analyzer uses instead of `cargo check` for\ndiagnostics on save. The command is required to output json and\nshould therefore include `--message-format=json` or a similar option\n(if your client supports the `colorDiagnosticOutput` experimental\ncapability, you can use `--message-format=json-diagnostic-rendered-ansi`).\n\nIf you're changing this because you're using some tool wrapping\nCargo, you might also want to change\n`#rust-analyzer.cargo.buildScripts.overrideCommand#`.\n\nIf there are multiple linked projects/workspaces, this command is invoked for\neach of them, with the working directory being the workspace root\n(i.e., the folder containing the `Cargo.toml`). This can be overwritten\nby changing `#rust-analyzer.check.invocationStrategy#`.\n\nIt supports two interpolation syntaxes, both mainly intended to be used with\n[non-Cargo build systems](./non_cargo_based_projects.md):\n\n- If `{saved_file}` is part of the command, rust-analyzer will pass\n the absolute path of the saved file to the provided command.\n (A previous version, `$saved_file`, also works.)\n- If `{label}` is part of the command, rust-analyzer will pass the\n Cargo package ID, which can be used with `cargo check -p`, or a build label from\n `rust-project.json`. If `{label}` is included, rust-analyzer behaves much like\n [`\"rust-analyzer.check.workspace\": false`](#check.workspace).\n\n\n\nAn example command would be:\n\n```bash\ncargo check --workspace --message-format=json --all-targets\n```\n\nNote: The option must be specified as an array of command line arguments, with\nthe first argument being the name of the command to run.",
"default": null,
"type": [
"null",
@@ -3135,7 +3135,7 @@
"title": "Workspace",
"properties": {
"rust-analyzer.workspace.discoverConfig": {
- "markdownDescription": "Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].\n\n[`DiscoverWorkspaceConfig`] also requires setting `progress_label` and `files_to_watch`.\n`progress_label` is used for the title in progress indicators, whereas `files_to_watch`\nis used to determine which build system-specific files should be watched in order to\nreload rust-analyzer.\n\nBelow is an example of a valid configuration:\n```json\n\"rust-analyzer.workspace.discoverConfig\": {\n \"command\": [\n \"rust-project\",\n \"develop-json\"\n ],\n \"progressLabel\": \"rust-analyzer\",\n \"filesToWatch\": [\n \"BUCK\"\n ]\n}\n```\n\n## On `DiscoverWorkspaceConfig::command`\n\n**Warning**: This format is provisional and subject to change.\n\n[`DiscoverWorkspaceConfig::command`] *must* return a JSON object corresponding to\n`DiscoverProjectData::Finished`:\n\n```norun\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\n#[serde(rename_all = \"snake_case\")]\nenum DiscoverProjectData {\n Finished { buildfile: Utf8PathBuf, project: ProjectJsonData },\n Error { error: String, source: Option<String> },\n Progress { message: String },\n}\n```\n\nAs JSON, `DiscoverProjectData::Finished` is:\n\n```json\n{\n // the internally-tagged representation of the enum.\n \"kind\": \"finished\",\n // the file used by a non-Cargo build system to define\n // a package or target.\n \"buildfile\": \"rust-analyzer/BUILD\",\n // the contents of a rust-project.json, elided for brevity\n \"project\": {\n \"sysroot\": \"foo\",\n \"crates\": []\n }\n}\n```\n\nIt is encouraged, but not required, to use the other variants on `DiscoverProjectData`\nto provide a more polished end-user experience.\n\n`DiscoverWorkspaceConfig::command` may *optionally* include an `{arg}`, which will be\nsubstituted with the JSON-serialized form of the following enum:\n\n```norun\n#[derive(PartialEq, Clone, Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum DiscoverArgument {\n Path(AbsPathBuf),\n Buildfile(AbsPathBuf),\n}\n```\n\nThe JSON representation of `DiscoverArgument::Path` is:\n\n```json\n{\n \"path\": \"src/main.rs\"\n}\n```\n\nSimilarly, the JSON representation of `DiscoverArgument::Buildfile` is:\n\n```json\n{\n \"buildfile\": \"BUILD\"\n}\n```\n\n`DiscoverArgument::Path` is used to find and generate a `rust-project.json`, and\ntherefore, a workspace, whereas `DiscoverArgument::buildfile` is used to to update an\nexisting workspace. As a reference for implementors, buck2's `rust-project` will likely\nbe useful: <https://github.com/facebook/buck2/tree/main/integrations/rust-project>.",
+ "markdownDescription": "Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].\n\n[`DiscoverWorkspaceConfig`] also requires setting `progressLabel` and `filesToWatch`.\n`progressLabel` is used for the title in progress indicators, whereas `filesToWatch`\nis used to determine which build system-specific files should be watched in order to\nreload rust-analyzer.\n\nBelow is an example of a valid configuration:\n```json\n\"rust-analyzer.workspace.discoverConfig\": {\n \"command\": [\n \"rust-project\",\n \"develop-json\",\n \"{arg}\"\n ],\n \"progressLabel\": \"buck2/rust-project\",\n \"filesToWatch\": [\n \"BUCK\"\n ]\n}\n```\n\n## Workspace Discovery Protocol\n\n**Warning**: This format is provisional and subject to change.\n\n[`DiscoverWorkspaceConfig::command`] *must* return a JSON object corresponding to\n`DiscoverProjectData::Finished`:\n\n```norun\n#[derive(Debug, Clone, Deserialize, Serialize)]\n#[serde(tag = \"kind\")]\n#[serde(rename_all = \"snake_case\")]\nenum DiscoverProjectData {\n Finished { buildfile: Utf8PathBuf, project: ProjectJsonData },\n Error { error: String, source: Option<String> },\n Progress { message: String },\n}\n```\n\nAs JSON, `DiscoverProjectData::Finished` is:\n\n```json\n{\n // the internally-tagged representation of the enum.\n \"kind\": \"finished\",\n // the file used by a non-Cargo build system to define\n // a package or target.\n \"buildfile\": \"rust-analyzer/BUILD\",\n // the contents of a rust-project.json, elided for brevity\n \"project\": {\n \"sysroot\": \"foo\",\n \"crates\": []\n }\n}\n```\n\nIt is encouraged, but not required, to use the other variants on `DiscoverProjectData`\nto provide a more polished end-user experience.\n\n`DiscoverWorkspaceConfig::command` may *optionally* include an `{arg}`, which will be\nsubstituted with the JSON-serialized form of the following enum:\n\n```norun\n#[derive(PartialEq, Clone, Debug, Serialize)]\n#[serde(rename_all = \"camelCase\")]\npub enum DiscoverArgument {\n Path(AbsPathBuf),\n Buildfile(AbsPathBuf),\n}\n```\n\nThe JSON representation of `DiscoverArgument::Path` is:\n\n```json\n{\n \"path\": \"src/main.rs\"\n}\n```\n\nSimilarly, the JSON representation of `DiscoverArgument::Buildfile` is:\n\n```json\n{\n \"buildfile\": \"BUILD\"\n}\n```\n\n`DiscoverArgument::Path` is used to find and generate a `rust-project.json`, and\ntherefore, a workspace, whereas `DiscoverArgument::buildfile` is used to to update an\nexisting workspace. As a reference for implementors, buck2's `rust-project` will likely\nbe useful: <https://github.com/facebook/buck2/tree/main/integrations/rust-project>.",
"default": null,
"anyOf": [
{