Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/project-model/src/lib.rs2
-rw-r--r--crates/project-model/src/project_json.rs177
-rw-r--r--crates/project-model/src/workspace.rs2
-rw-r--r--crates/rust-analyzer/src/global_state.rs61
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs70
-rw-r--r--crates/rust-analyzer/src/lib.rs2
-rw-r--r--crates/rust-analyzer/src/lsp/ext.rs27
-rw-r--r--crates/rust-analyzer/src/lsp/to_proto.rs184
-rw-r--r--crates/rust-analyzer/src/target_spec.rs (renamed from crates/rust-analyzer/src/cargo_target_spec.rs)110
-rw-r--r--docs/dev/lsp-extensions.md15
-rw-r--r--editors/code/src/commands.ts7
-rw-r--r--editors/code/src/debug.ts40
-rw-r--r--editors/code/src/lsp_ext.ts30
-rw-r--r--editors/code/src/run.ts76
-rw-r--r--editors/code/src/tasks.ts47
-rw-r--r--editors/code/src/util.ts7
-rw-r--r--editors/code/tests/unit/runnable_env.test.ts4
17 files changed, 631 insertions, 230 deletions
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index 181c07f46b..35643dcc02 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -22,7 +22,7 @@ mod cargo_workspace;
mod cfg;
mod env;
mod manifest_path;
-mod project_json;
+pub mod project_json;
mod rustc_cfg;
mod sysroot;
pub mod target_data_layout;
diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs
index 408593ea8a..4a916e570b 100644
--- a/crates/project-model/src/project_json.rs
+++ b/crates/project-model/src/project_json.rs
@@ -33,7 +33,7 @@
//!
//! * file on disk
//! * a field in the config (ie, you can send a JSON request with the contents
-//! of rust-project.json to rust-analyzer, no need to write anything to disk)
+//! of `rust-project.json` to rust-analyzer, no need to write anything to disk)
//!
//! Another possible thing we don't do today, but which would be totally valid,
//! is to add an extension point to VS Code extension to register custom
@@ -55,8 +55,7 @@ use rustc_hash::FxHashMap;
use serde::{de, Deserialize, Serialize};
use span::Edition;
-use crate::cfg::CfgFlag;
-use crate::ManifestPath;
+use crate::{cfg::CfgFlag, ManifestPath, TargetKind};
/// Roots and crates that compose this Rust project.
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -68,6 +67,10 @@ pub struct ProjectJson {
project_root: AbsPathBuf,
manifest: Option<ManifestPath>,
crates: Vec<Crate>,
+ /// Configuration for CLI commands.
+ ///
+ /// Examples include a check build or a test run.
+ runnables: Vec<Runnable>,
}
/// A crate points to the root module of a crate and lists the dependencies of the crate. This is
@@ -88,6 +91,86 @@ pub struct Crate {
pub(crate) exclude: Vec<AbsPathBuf>,
pub(crate) is_proc_macro: bool,
pub(crate) repository: Option<String>,
+ pub build: Option<Build>,
+}
+
+/// Additional, build-specific data about a crate.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Build {
+ /// The name associated with this crate.
+ ///
+ /// This is determined by the build system that produced
+ /// the `rust-project.json` in question. For instance, if buck were used,
+ /// the label might be something like `//ide/rust/rust-analyzer:rust-analyzer`.
+ ///
+ /// Do not attempt to parse the contents of this string; it is a build system-specific
+ /// identifier similar to [`Crate::display_name`].
+ pub label: String,
+ /// Path corresponding to the build system-specific file defining the crate.
+ ///
+ /// It is roughly analogous to [`ManifestPath`], but it should *not* be used with
+ /// [`crate::ProjectManifest::from_manifest_file`], as the build file may not be
+ /// be in the `rust-project.json`.
+ pub build_file: Utf8PathBuf,
+ /// The kind of target.
+ ///
+ /// Examples (non-exhaustively) include [`TargetKind::Bin`], [`TargetKind::Lib`],
+ /// and [`TargetKind::Test`]. This information is used to determine what sort
+ /// of runnable codelens to provide, if any.
+ pub target_kind: TargetKind,
+}
+
+/// A template-like structure for describing runnables.
+///
+/// These are used for running and debugging binaries and tests without encoding
+/// build system-specific knowledge into rust-analyzer.
+///
+/// # Example
+///
+/// Below is an example of a test runnable. `{label}` and `{test_id}`
+/// are explained in [`Runnable::args`]'s documentation.
+///
+/// ```json
+/// {
+/// "program": "buck",
+/// "args": [
+/// "test",
+/// "{label}",
+/// "--",
+/// "{test_id}",
+/// "--print-passing-details"
+/// ],
+/// "cwd": "/home/user/repo-root/",
+/// "kind": "testOne"
+/// }
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Runnable {
+ /// The program invoked by the runnable.
+ ///
+ /// For example, this might be `cargo`, `buck`, or `bazel`.
+ pub program: String,
+ /// The arguments passed to [`Runnable::program`].
+ ///
+ /// The args can contain two template strings: `{label}` and `{test_id}`.
+ /// rust-analyzer will find and replace `{label}` with [`Build::label`] and
+ /// `{test_id}` with the test name.
+ pub args: Vec<String>,
+ /// The current working directory of the runnable.
+ pub cwd: Utf8PathBuf,
+ pub kind: RunnableKind,
+}
+
+/// The kind of runnable.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RunnableKind {
+ Check,
+
+ /// Can run a binary.
+ Run,
+
+ /// Run a single test.
+ TestOne,
}
impl ProjectJson {
@@ -95,6 +178,7 @@ impl ProjectJson {
///
/// # Arguments
///
+ /// * `manifest` - The path to the `rust-project.json`.
/// * `base` - The path to the workspace root (i.e. the folder containing `rust-project.json`)
/// * `data` - The parsed contents of `rust-project.json`, or project json that's passed via
/// configuration.
@@ -109,6 +193,7 @@ impl ProjectJson {
sysroot_src: data.sysroot_src.map(absolutize_on_base),
project_root: base.to_path_buf(),
manifest,
+ runnables: data.runnables.into_iter().map(Runnable::from).collect(),
crates: data
.crates
.into_iter()
@@ -127,6 +212,15 @@ impl ProjectJson {
None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()),
};
+ let build = match crate_data.build {
+ Some(build) => Some(Build {
+ label: build.label,
+ build_file: build.build_file,
+ target_kind: build.target_kind.into(),
+ }),
+ None => None,
+ };
+
Crate {
display_name: crate_data
.display_name
@@ -146,6 +240,7 @@ impl ProjectJson {
exclude,
is_proc_macro: crate_data.is_proc_macro,
repository: crate_data.repository,
+ build,
}
})
.collect(),
@@ -167,7 +262,15 @@ impl ProjectJson {
&self.project_root
}
- /// Returns the path to the project's manifest file, if it exists.
+ 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.
pub fn manifest(&self) -> Option<&ManifestPath> {
self.manifest.as_ref()
}
@@ -176,6 +279,10 @@ impl ProjectJson {
pub fn manifest_or_root(&self) -> &AbsPath {
self.manifest.as_ref().map_or(&self.project_root, |manifest| manifest.as_ref())
}
+
+ pub fn runnables(&self) -> &[Runnable] {
+ &self.runnables
+ }
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -183,6 +290,8 @@ pub struct ProjectJsonData {
sysroot: Option<Utf8PathBuf>,
sysroot_src: Option<Utf8PathBuf>,
crates: Vec<CrateData>,
+ #[serde(default)]
+ runnables: Vec<RunnableData>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -205,6 +314,8 @@ struct CrateData {
is_proc_macro: bool,
#[serde(default)]
repository: Option<String>,
+ #[serde(default)]
+ build: Option<BuildData>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -220,6 +331,48 @@ enum EditionData {
Edition2024,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct BuildData {
+ label: String,
+ build_file: Utf8PathBuf,
+ target_kind: TargetKindData,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RunnableData {
+ pub program: String,
+ pub args: Vec<String>,
+ pub cwd: Utf8PathBuf,
+ pub kind: RunnableKindData,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum RunnableKindData {
+ Check,
+ Run,
+ TestOne,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum TargetKindData {
+ Bin,
+ /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
+ Lib,
+ Test,
+}
+
+impl From<TargetKindData> for TargetKind {
+ fn from(data: TargetKindData) -> Self {
+ match data {
+ TargetKindData::Bin => TargetKind::Bin,
+ TargetKindData::Lib => TargetKind::Lib { is_proc_macro: false },
+ TargetKindData::Test => TargetKind::Test,
+ }
+ }
+}
+
impl From<EditionData> for Edition {
fn from(data: EditionData) -> Self {
match data {
@@ -231,6 +384,22 @@ impl From<EditionData> for Edition {
}
}
+impl From<RunnableData> for Runnable {
+ fn from(data: RunnableData) -> Self {
+ Runnable { program: data.program, args: data.args, cwd: data.cwd, kind: data.kind.into() }
+ }
+}
+
+impl From<RunnableKindData> for RunnableKind {
+ fn from(data: RunnableKindData) -> Self {
+ match data {
+ RunnableKindData::Check => RunnableKind::Check,
+ RunnableKindData::Run => RunnableKind::Run,
+ RunnableKindData::TestOne => RunnableKind::TestOne,
+ }
+ }
+}
+
/// Identifies a crate by position in the crates array.
///
/// This will differ from `CrateId` when multiple `ProjectJson`
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index 4dba11eac3..17e40e74de 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -76,7 +76,7 @@ pub enum ProjectWorkspaceKind {
/// Environment variables set in the `.cargo/config` file.
cargo_config_extra_env: FxHashMap<String, String>,
},
- /// Project workspace was manually specified using a `rust-project.json` file.
+ /// Project workspace was specified using a `rust-project.json` file.
Json(ProjectJson),
// FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning.
// That's not the end user experience we should strive for.
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index 3d5f525aaf..717d8a632c 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -18,10 +18,7 @@ use parking_lot::{
RwLockWriteGuard,
};
use proc_macro_api::ProcMacroServer;
-use project_model::{
- CargoWorkspace, ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, Target,
- WorkspaceBuildScripts,
-};
+use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{span, Level};
use triomphe::Arc;
@@ -40,6 +37,7 @@ use crate::{
mem_docs::MemDocs,
op_queue::OpQueue,
reload,
+ target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec},
task_pool::{TaskPool, TaskQueue},
};
@@ -556,21 +554,52 @@ impl GlobalStateSnapshot {
self.vfs_read().file_path(file_id).clone()
}
- pub(crate) fn cargo_target_for_crate_root(
- &self,
- crate_id: CrateId,
- ) -> Option<(&CargoWorkspace, Target)> {
+ pub(crate) fn target_spec_for_crate(&self, crate_id: CrateId) -> Option<TargetSpec> {
let file_id = self.analysis.crate_root(crate_id).ok()?;
let path = self.vfs_read().file_path(file_id).clone();
let path = path.as_path()?;
- self.workspaces.iter().find_map(|ws| match &ws.kind {
- ProjectWorkspaceKind::Cargo { cargo, .. }
- | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
- cargo.target_by_root(path).map(|it| (cargo, it))
- }
- ProjectWorkspaceKind::Json { .. } => None,
- ProjectWorkspaceKind::DetachedFile { .. } => None,
- })
+
+ for workspace in self.workspaces.iter() {
+ match &workspace.kind {
+ ProjectWorkspaceKind::Cargo { cargo, .. }
+ | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
+ let Some(target_idx) = cargo.target_by_root(path) else {
+ continue;
+ };
+
+ let target_data = &cargo[target_idx];
+ let package_data = &cargo[target_data.package];
+
+ return Some(TargetSpec::Cargo(CargoTargetSpec {
+ workspace_root: cargo.workspace_root().to_path_buf(),
+ cargo_toml: package_data.manifest.clone(),
+ crate_id,
+ package: cargo.package_flag(package_data),
+ target: target_data.name.clone(),
+ target_kind: target_data.kind,
+ required_features: target_data.required_features.clone(),
+ features: package_data.features.keys().cloned().collect(),
+ }));
+ }
+ ProjectWorkspaceKind::Json(project) => {
+ let Some(krate) = project.crate_by_root(path) else {
+ continue;
+ };
+ let Some(build) = krate.build else {
+ continue;
+ };
+
+ return Some(TargetSpec::ProjectJson(ProjectJsonTargetSpec {
+ label: build.label,
+ target_kind: build.target_kind,
+ shell_runnables: project.runnables().to_owned(),
+ }));
+ }
+ ProjectWorkspaceKind::DetachedFile { .. } => {}
+ };
+ }
+
+ None
}
pub(crate) fn file_exists(&self, file_id: FileId) -> bool {
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index 0789dd6462..8e39b15da3 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -35,7 +35,6 @@ use triomphe::Arc;
use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
use crate::{
- cargo_target_spec::CargoTargetSpec,
config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
diff::diff,
global_state::{GlobalState, GlobalStateSnapshot},
@@ -51,6 +50,7 @@ use crate::{
self, CrateInfoResult, ExternalDocsPair, ExternalDocsResponse, FetchDependencyListParams,
FetchDependencyListResult, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams,
},
+ target_spec::TargetSpec,
};
pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> anyhow::Result<()> {
@@ -790,9 +790,9 @@ pub(crate) fn handle_parent_module(
Some(&crate_id) => crate_id,
None => return Ok(None),
};
- let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? {
- Some(it) => it,
- None => return Ok(None),
+ let cargo_spec = match TargetSpec::for_file(&snap, file_id)? {
+ Some(TargetSpec::Cargo(it)) => it,
+ Some(TargetSpec::ProjectJson(_)) | None => return Ok(None),
};
if snap.analysis.crate_root(crate_id)? == file_id {
@@ -823,7 +823,7 @@ pub(crate) fn handle_runnables(
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.file_line_index(file_id)?;
let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok());
- let cargo_spec = CargoTargetSpec::for_file(&snap, file_id)?;
+ let target_spec = TargetSpec::for_file(&snap, file_id)?;
let expect_test = match offset {
Some(offset) => {
@@ -840,21 +840,24 @@ pub(crate) fn handle_runnables(
if should_skip_for_offset(&runnable, offset) {
continue;
}
- if should_skip_target(&runnable, cargo_spec.as_ref()) {
+ if should_skip_target(&runnable, target_spec.as_ref()) {
continue;
}
- let mut runnable = to_proto::runnable(&snap, runnable)?;
- if expect_test {
- runnable.label = format!("{} + expect", runnable.label);
- runnable.args.expect_test = Some(true);
+ if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? {
+ if expect_test {
+ if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args {
+ runnable.label = format!("{} + expect", runnable.label);
+ r.expect_test = Some(true);
+ }
+ }
+ res.push(runnable);
}
- res.push(runnable);
}
// Add `cargo check` and `cargo test` for all targets of the whole package
let config = snap.config.runnables();
- match cargo_spec {
- Some(spec) => {
+ match target_spec {
+ Some(TargetSpec::Cargo(spec)) => {
let is_crate_no_std = snap.analysis.is_crate_no_std(spec.crate_id)?;
for cmd in ["check", "run", "test"] {
if cmd == "run" && spec.target_kind != TargetKind::Bin {
@@ -879,7 +882,7 @@ pub(crate) fn handle_runnables(
),
location: None,
kind: lsp_ext::RunnableKind::Cargo,
- args: lsp_ext::CargoRunnable {
+ args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
workspace_root: Some(spec.workspace_root.clone().into()),
cwd: Some(cwd.into()),
override_cargo: config.override_cargo.clone(),
@@ -887,17 +890,18 @@ pub(crate) fn handle_runnables(
cargo_extra_args: config.cargo_extra_args.clone(),
executable_args: Vec::new(),
expect_test: None,
- },
+ }),
})
}
}
+ Some(TargetSpec::ProjectJson(_)) => {}
None => {
if !snap.config.linked_or_discovered_projects().is_empty() {
res.push(lsp_ext::Runnable {
label: "cargo check --workspace".to_owned(),
location: None,
kind: lsp_ext::RunnableKind::Cargo,
- args: lsp_ext::CargoRunnable {
+ args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
workspace_root: None,
cwd: None,
override_cargo: config.override_cargo,
@@ -905,7 +909,7 @@ pub(crate) fn handle_runnables(
cargo_extra_args: config.cargo_extra_args,
executable_args: Vec::new(),
expect_test: None,
- },
+ }),
});
}
}
@@ -931,7 +935,7 @@ pub(crate) fn handle_related_tests(
let tests = snap.analysis.related_tests(position, None)?;
let mut res = Vec::new();
for it in tests {
- if let Ok(runnable) = to_proto::runnable(&snap, it) {
+ if let Ok(Some(runnable)) = to_proto::runnable(&snap, it) {
res.push(lsp_ext::TestInfo { runnable })
}
}
@@ -1397,14 +1401,14 @@ pub(crate) fn handle_code_lens(
}
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let cargo_target_spec = CargoTargetSpec::for_file(&snap, file_id)?;
+ let target_spec = TargetSpec::for_file(&snap, file_id)?;
let annotations = snap.analysis.annotations(
&AnnotationConfig {
- binary_target: cargo_target_spec
+ binary_target: target_spec
.map(|spec| {
matches!(
- spec.target_kind,
+ spec.target_kind(),
TargetKind::Bin | TargetKind::Example | TargetKind::Test
)
})
@@ -1824,9 +1828,9 @@ pub(crate) fn handle_open_cargo_toml(
let _p = tracing::info_span!("handle_open_cargo_toml").entered();
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
- let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? {
- Some(it) => it,
- None => return Ok(None),
+ let cargo_spec = match TargetSpec::for_file(&snap, file_id)? {
+ Some(TargetSpec::Cargo(it)) => it,
+ Some(TargetSpec::ProjectJson(_)) | None => return Ok(None),
};
let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml);
@@ -1954,8 +1958,8 @@ fn runnable_action_links(
return None;
}
- let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
- if should_skip_target(&runnable, cargo_spec.as_ref()) {
+ let target_spec = TargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
+ if should_skip_target(&runnable, target_spec.as_ref()) {
return None;
}
@@ -1965,7 +1969,7 @@ fn runnable_action_links(
}
let title = runnable.title();
- let r = to_proto::runnable(snap, runnable).ok()?;
+ let r = to_proto::runnable(snap, runnable).ok()??;
let mut group = lsp_ext::CommandLinkGroup::default();
@@ -2020,13 +2024,13 @@ fn prepare_hover_actions(
.collect()
}
-fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>) -> bool {
+fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&TargetSpec>) -> bool {
match runnable.kind {
RunnableKind::Bin => {
// Do not suggest binary run on other target than binary
match &cargo_spec {
Some(spec) => !matches!(
- spec.target_kind,
+ spec.target_kind(),
TargetKind::Bin | TargetKind::Example | TargetKind::Test
),
None => true,
@@ -2103,9 +2107,9 @@ fn run_rustfmt(
}
RustfmtConfig::CustomCommand { command, args } => {
let cmd = Utf8PathBuf::from(&command);
- let workspace = CargoTargetSpec::for_file(snap, file_id)?;
- let mut cmd = match workspace {
- Some(spec) => {
+ let target_spec = TargetSpec::for_file(snap, file_id)?;
+ let mut cmd = match target_spec {
+ Some(TargetSpec::Cargo(spec)) => {
// approach: if the command name contains a path separator, join it with the workspace root.
// however, if the path is absolute, joining will result in the absolute path being preserved.
// as a fallback, rely on $PATH-based discovery.
@@ -2118,7 +2122,7 @@ fn run_rustfmt(
};
process::Command::new(cmd_path)
}
- None => process::Command::new(cmd),
+ _ => process::Command::new(cmd),
};
cmd.envs(snap.config.extra_env());
diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs
index b3c11d0156..a398e98f09 100644
--- a/crates/rust-analyzer/src/lib.rs
+++ b/crates/rust-analyzer/src/lib.rs
@@ -14,7 +14,6 @@
pub mod cli;
mod caps;
-mod cargo_target_spec;
mod diagnostics;
mod diff;
mod dispatch;
@@ -24,6 +23,7 @@ mod main_loop;
mod mem_docs;
mod op_queue;
mod reload;
+mod target_spec;
mod task_pool;
mod version;
diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs
index 4da9054d13..b82ba44190 100644
--- a/crates/rust-analyzer/src/lsp/ext.rs
+++ b/crates/rust-analyzer/src/lsp/ext.rs
@@ -3,7 +3,6 @@
#![allow(clippy::disallowed_types)]
use std::ops;
-use std::path::PathBuf;
use ide_db::line_index::WideEncoding;
use lsp_types::request::Request;
@@ -12,6 +11,7 @@ use lsp_types::{
PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
};
use lsp_types::{PositionEncodingKind, Url};
+use paths::Utf8PathBuf;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
@@ -439,24 +439,33 @@ pub struct Runnable {
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<lsp_types::LocationLink>,
pub kind: RunnableKind,
- pub args: CargoRunnable,
+ pub args: RunnableArgs,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+#[serde(untagged)]
+pub enum RunnableArgs {
+ Cargo(CargoRunnableArgs),
+ Shell(ShellRunnableArgs),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum RunnableKind {
Cargo,
+ Shell,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
-pub struct CargoRunnable {
+pub struct CargoRunnableArgs {
// command to be executed instead of cargo
pub override_cargo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub workspace_root: Option<PathBuf>,
+ pub workspace_root: Option<Utf8PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub cwd: Option<PathBuf>,
+ pub cwd: Option<Utf8PathBuf>,
// command, --package and --lib stuff
pub cargo_args: Vec<String>,
// user-specified additional cargo args, like `--release`.
@@ -467,6 +476,14 @@ pub struct CargoRunnable {
pub expect_test: Option<bool>,
}
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ShellRunnableArgs {
+ pub program: String,
+ pub args: Vec<String>,
+ pub cwd: Utf8PathBuf,
+}
+
pub enum RelatedTests {}
impl Request for RelatedTests {
diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs
index 86368c9eea..db5f666a5b 100644
--- a/crates/rust-analyzer/src/lsp/to_proto.rs
+++ b/crates/rust-analyzer/src/lsp/to_proto.rs
@@ -21,16 +21,17 @@ use serde_json::to_value;
use vfs::AbsPath;
use crate::{
- cargo_target_spec::CargoTargetSpec,
config::{CallInfoConfig, Config},
global_state::GlobalStateSnapshot,
line_index::{LineEndings, LineIndex, PositionEncoding},
lsp::{
+ ext::ShellRunnableArgs,
semantic_tokens::{self, standard_fallback_type},
utils::invalid_params_error,
LspError,
},
lsp_ext::{self, SnippetTextEdit},
+ target_spec::{CargoTargetSpec, TargetSpec},
};
pub(crate) fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position {
@@ -1356,34 +1357,90 @@ pub(crate) fn code_action(
pub(crate) fn runnable(
snap: &GlobalStateSnapshot,
runnable: Runnable,
-) -> Cancellable<lsp_ext::Runnable> {
+) -> Cancellable<Option<lsp_ext::Runnable>> {
let config = snap.config.runnables();
- let spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id)?;
- let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone());
- let cwd = match runnable.kind {
- ide::RunnableKind::Bin { .. } => workspace_root.clone().map(|it| it.into()),
- _ => spec.as_ref().map(|it| it.cargo_toml.parent().into()),
- };
- let target = spec.as_ref().map(|s| s.target.as_str());
- let label = runnable.label(target);
- let (cargo_args, executable_args) =
- CargoTargetSpec::runnable_args(snap, spec, &runnable.kind, &runnable.cfg);
- let location = location_link(snap, None, runnable.nav)?;
+ let target_spec = TargetSpec::for_file(snap, runnable.nav.file_id)?;
- Ok(lsp_ext::Runnable {
- label,
- location: Some(location),
- kind: lsp_ext::RunnableKind::Cargo,
- args: lsp_ext::CargoRunnable {
- workspace_root: workspace_root.map(|it| it.into()),
- cwd,
- override_cargo: config.override_cargo,
- cargo_args,
- cargo_extra_args: config.cargo_extra_args,
- executable_args,
- expect_test: None,
- },
- })
+ match target_spec {
+ Some(TargetSpec::Cargo(spec)) => {
+ let workspace_root = spec.workspace_root.clone();
+
+ let target = spec.target.clone();
+
+ let (cargo_args, executable_args) = CargoTargetSpec::runnable_args(
+ snap,
+ Some(spec.clone()),
+ &runnable.kind,
+ &runnable.cfg,
+ );
+
+ let cwd = match runnable.kind {
+ ide::RunnableKind::Bin { .. } => workspace_root.clone(),
+ _ => spec.cargo_toml.parent().to_owned(),
+ };
+
+ let label = runnable.label(Some(&target));
+ let location = location_link(snap, None, runnable.nav)?;
+
+ Ok(Some(lsp_ext::Runnable {
+ label,
+ location: Some(location),
+ kind: lsp_ext::RunnableKind::Cargo,
+ args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
+ workspace_root: Some(workspace_root.into()),
+ override_cargo: config.override_cargo,
+ cargo_args,
+ cwd: Some(cwd.into()),
+ cargo_extra_args: config.cargo_extra_args,
+ executable_args,
+ expect_test: None,
+ }),
+ }))
+ }
+ Some(TargetSpec::ProjectJson(spec)) => {
+ let label = runnable.label(Some(&spec.label));
+ let location = location_link(snap, None, runnable.nav)?;
+
+ match spec.runnable_args(&runnable.kind) {
+ Some(json_shell_runnable_args) => {
+ let runnable_args = ShellRunnableArgs {
+ program: json_shell_runnable_args.program,
+ args: json_shell_runnable_args.args,
+ cwd: json_shell_runnable_args.cwd,
+ };
+ Ok(Some(lsp_ext::Runnable {
+ label,
+ location: Some(location),
+ kind: lsp_ext::RunnableKind::Shell,
+ args: lsp_ext::RunnableArgs::Shell(runnable_args),
+ }))
+ }
+ None => Ok(None),
+ }
+ }
+ None => {
+ let (cargo_args, executable_args) =
+ CargoTargetSpec::runnable_args(snap, None, &runnable.kind, &runnable.cfg);
+
+ let label = runnable.label(None);
+ let location = location_link(snap, None, runnable.nav)?;
+
+ Ok(Some(lsp_ext::Runnable {
+ label,
+ location: Some(location),
+ kind: lsp_ext::RunnableKind::Cargo,
+ args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
+ workspace_root: None,
+ override_cargo: config.override_cargo,
+ cargo_args,
+ cwd: None,
+ cargo_extra_args: config.cargo_extra_args,
+ executable_args,
+ expect_test: None,
+ }),
+ }))
+ }
+ }
}
pub(crate) fn code_lens(
@@ -1407,33 +1464,37 @@ pub(crate) fn code_lens(
};
let r = runnable(snap, run)?;
- let lens_config = snap.config.lens();
- if lens_config.run
- && client_commands_config.run_single
- && r.args.workspace_root.is_some()
- {
- let command = command::run_single(&r, &title);
- acc.push(lsp_types::CodeLens {
- range: annotation_range,
- command: Some(command),
- data: None,
- })
- }
- if lens_config.debug && can_debug && client_commands_config.debug_single {
- let command = command::debug_single(&r);
- acc.push(lsp_types::CodeLens {
- range: annotation_range,
- command: Some(command),
- data: None,
- })
- }
- if lens_config.interpret {
- let command = command::interpret_single(&r);
- acc.push(lsp_types::CodeLens {
- range: annotation_range,
- command: Some(command),
- data: None,
- })
+ if let Some(r) = r {
+ let has_root = match &r.args {
+ lsp_ext::RunnableArgs::Cargo(c) => c.workspace_root.is_some(),
+ lsp_ext::RunnableArgs::Shell(_) => true,
+ };
+
+ let lens_config = snap.config.lens();
+ if lens_config.run && client_commands_config.run_single && has_root {
+ let command = command::run_single(&r, &title);
+ acc.push(lsp_types::CodeLens {
+ range: annotation_range,
+ command: Some(command),
+ data: None,
+ })
+ }
+ if lens_config.debug && can_debug && client_commands_config.debug_single {
+ let command = command::debug_single(&r);
+ acc.push(lsp_types::CodeLens {
+ range: annotation_range,
+ command: Some(command),
+ data: None,
+ })
+ }
+ if lens_config.interpret {
+ let command = command::interpret_single(&r);
+ acc.push(lsp_types::CodeLens {
+ range: annotation_range,
+ command: Some(command),
+ data: None,
+ })
+ }
}
}
AnnotationKind::HasImpls { pos, data } => {
@@ -1538,12 +1599,8 @@ pub(crate) fn test_item(
id: test_item.id,
label: test_item.label,
kind: match test_item.kind {
- ide::TestItemKind::Crate(id) => 'b: {
- let Some((cargo_ws, target)) = snap.cargo_target_for_crate_root(id) else {
- break 'b lsp_ext::TestItemKind::Package;
- };
- let target = &cargo_ws[target];
- match target.kind {
+ ide::TestItemKind::Crate(id) => match snap.target_spec_for_crate(id) {
+ Some(target_spec) => match target_spec.target_kind() {
project_model::TargetKind::Bin
| project_model::TargetKind::Lib { .. }
| project_model::TargetKind::Example
@@ -1552,8 +1609,9 @@ pub(crate) fn test_item(
project_model::TargetKind::Test => lsp_ext::TestItemKind::Test,
// benches are not tests needed to be shown in the test explorer
project_model::TargetKind::Bench => return None,
- }
- }
+ },
+ None => lsp_ext::TestItemKind::Package,
+ },
ide::TestItemKind::Module => lsp_ext::TestItemKind::Module,
ide::TestItemKind::Function => lsp_ext::TestItemKind::Test,
},
@@ -1566,7 +1624,7 @@ pub(crate) fn test_item(
.file
.map(|f| lsp_types::TextDocumentIdentifier { uri: url(snap, f) }),
range: line_index.and_then(|l| Some(range(l, test_item.text_range?))),
- runnable: test_item.runnable.and_then(|r| runnable(snap, r).ok()),
+ runnable: test_item.runnable.and_then(|r| runnable(snap, r).ok()).flatten(),
})
}
diff --git a/crates/rust-analyzer/src/cargo_target_spec.rs b/crates/rust-analyzer/src/target_spec.rs
index 693a35b91e..6145f7e05f 100644
--- a/crates/rust-analyzer/src/cargo_target_spec.rs
+++ b/crates/rust-analyzer/src/target_spec.rs
@@ -1,20 +1,52 @@
-//! See `CargoTargetSpec`
+//! See `TargetSpec`
use std::mem;
use cfg::{CfgAtom, CfgExpr};
use ide::{Cancellable, CrateId, FileId, RunnableKind, TestId};
+use project_model::project_json::Runnable;
use project_model::{CargoFeatures, ManifestPath, TargetKind};
use rustc_hash::FxHashSet;
use vfs::AbsPathBuf;
use crate::global_state::GlobalStateSnapshot;
+/// A target represents a thing we can build or test.
+///
+/// We use it to calculate the CLI arguments required to build, run or
+/// test the target.
+#[derive(Clone, Debug)]
+pub(crate) enum TargetSpec {
+ Cargo(CargoTargetSpec),
+ ProjectJson(ProjectJsonTargetSpec),
+}
+
+impl TargetSpec {
+ pub(crate) fn for_file(
+ global_state_snapshot: &GlobalStateSnapshot,
+ file_id: FileId,
+ ) -> Cancellable<Option<Self>> {
+ let crate_id = match &*global_state_snapshot.analysis.crates_for(file_id)? {
+ &[crate_id, ..] => crate_id,
+ _ => return Ok(None),
+ };
+
+ Ok(global_state_snapshot.target_spec_for_crate(crate_id))
+ }
+
+ pub(crate) fn target_kind(&self) -> TargetKind {
+ match self {
+ TargetSpec::Cargo(cargo) => cargo.target_kind,
+ TargetSpec::ProjectJson(project_json) => project_json.target_kind,
+ }
+ }
+}
+
/// Abstract representation of Cargo target.
///
/// We use it to cook up the set of cli args we need to pass to Cargo to
/// build/test/run the target.
-#[derive(Clone)]
+#[derive(Clone, Debug)]
pub(crate) struct CargoTargetSpec {
pub(crate) workspace_root: AbsPathBuf,
pub(crate) cargo_toml: ManifestPath,
@@ -26,6 +58,51 @@ pub(crate) struct CargoTargetSpec {
pub(crate) features: FxHashSet<String>,
}
+#[derive(Clone, Debug)]
+pub(crate) struct ProjectJsonTargetSpec {
+ pub(crate) label: String,
+ pub(crate) target_kind: TargetKind,
+ pub(crate) shell_runnables: Vec<Runnable>,
+}
+
+impl ProjectJsonTargetSpec {
+ pub(crate) fn runnable_args(&self, kind: &RunnableKind) -> Option<Runnable> {
+ match kind {
+ RunnableKind::Bin => {
+ for runnable in &self.shell_runnables {
+ if matches!(runnable.kind, project_model::project_json::RunnableKind::Run) {
+ return Some(runnable.clone());
+ }
+ }
+
+ None
+ }
+ RunnableKind::Test { test_id, .. } => {
+ for runnable in &self.shell_runnables {
+ if matches!(runnable.kind, project_model::project_json::RunnableKind::TestOne) {
+ let mut runnable = runnable.clone();
+
+ let replaced_args: Vec<_> = runnable
+ .args
+ .iter()
+ .map(|arg| arg.replace("{test_id}", &test_id.to_string()))
+ .map(|arg| arg.replace("{label}", &self.label))
+ .collect();
+ runnable.args = replaced_args;
+
+ return Some(runnable);
+ }
+ }
+
+ None
+ }
+ RunnableKind::TestMod { .. } => None,
+ RunnableKind::Bench { .. } => None,
+ RunnableKind::DocTest { .. } => None,
+ }
+ }
+}
+
impl CargoTargetSpec {
pub(crate) fn runnable_args(
snap: &GlobalStateSnapshot,
@@ -122,35 +199,6 @@ impl CargoTargetSpec {
(cargo_args, executable_args)
}
- pub(crate) fn for_file(
- global_state_snapshot: &GlobalStateSnapshot,
- file_id: FileId,
- ) -> Cancellable<Option<CargoTargetSpec>> {
- let crate_id = match &*global_state_snapshot.analysis.crates_for(file_id)? {
- &[crate_id, ..] => crate_id,
- _ => return Ok(None),
- };
- let (cargo_ws, target) = match global_state_snapshot.cargo_target_for_crate_root(crate_id) {
- Some(it) => it,
- None => return Ok(None),
- };
-
- let target_data = &cargo_ws[target];
- let package_data = &cargo_ws[target_data.package];
- let res = CargoTargetSpec {
- workspace_root: cargo_ws.workspace_root().to_path_buf(),
- cargo_toml: package_data.manifest.clone(),
- package: cargo_ws.package_flag(package_data),
- target: target_data.name.clone(),
- target_kind: target_data.kind,
- required_features: target_data.required_features.clone(),
- features: package_data.features.keys().cloned().collect(),
- crate_id,
- };
-
- Ok(Some(res))
- }
-
pub(crate) fn push_to(self, buf: &mut Vec<String>, kind: &RunnableKind) {
buf.push("--package".to_owned());
buf.push(self.package);
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 100662f4ce..695fec7e8e 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
<!---
-lsp/ext.rs hash: a85ec97f07c6a2e3
+lsp/ext.rs hash: 8e6e340f2899b5e9
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@@ -372,7 +372,7 @@ interface Runnable {
}
```
-rust-analyzer supports only one `kind`, `"cargo"`. The `args` for `"cargo"` look like this:
+rust-analyzer supports two `kind`s of runnables, `"cargo"` and `"shell"`. The `args` for `"cargo"` look like this:
```typescript
{
@@ -386,6 +386,17 @@ rust-analyzer supports only one `kind`, `"cargo"`. The `args` for `"cargo"` look
}
```
+The args for `"shell"` look like this:
+
+```typescript
+{
+ kind: string;
+ program: string;
+ args: string[];
+ cwd: string;
+}
+```
+
## Test explorer
**Experimental Client Capability:** `{ "testExplorer": boolean }`
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 9f4930c94a..5cec2c61a5 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -9,10 +9,11 @@ import {
applySnippetTextEdits,
type SnippetTextDocumentEdit,
} from "./snippets";
-import { type RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
+import { type RunnableQuickPick, selectRunnable, createTask, createCargoArgs } from "./run";
import { AstInspector } from "./ast_inspector";
import {
isRustDocument,
+ isCargoRunnableArgs,
isCargoTomlDocument,
sleep,
isRustEditor,
@@ -1154,8 +1155,8 @@ export function copyRunCommandLine(ctx: CtxInit) {
let prevRunnable: RunnableQuickPick | undefined;
return async () => {
const item = await selectRunnable(ctx, prevRunnable);
- if (!item) return;
- const args = createArgs(item.runnable);
+ if (!item || !isCargoRunnableArgs(item.runnable.args)) return;
+ const args = createCargoArgs(item.runnable.args);
const commandLine = ["cargo", ...args].join(" ");
await vscode.env.clipboard.writeText(commandLine);
await vscode.window.showInformationMessage("Cargo invocation copied to the clipboard.");
diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts
index 4b96e4d5c8..eef2f6f4ee 100644
--- a/editors/code/src/debug.ts
+++ b/editors/code/src/debug.ts
@@ -7,10 +7,12 @@ import { Cargo, getRustcId, getSysroot } from "./toolchain";
import type { Ctx } from "./ctx";
import { prepareEnv } from "./run";
import { unwrapUndefinable } from "./undefinable";
+import { isCargoRunnableArgs } from "./util";
const debugOutput = vscode.window.createOutputChannel("Debug");
type DebugConfigProvider = (
- config: ra.Runnable,
+ runnable: ra.Runnable,
+ runnableArgs: ra.CargoRunnableArgs,
executable: string,
env: Record<string, string>,
sourceFileMap?: Record<string, string>,
@@ -76,6 +78,11 @@ async function getDebugConfiguration(
ctx: Ctx,
runnable: ra.Runnable,
): Promise<vscode.DebugConfiguration | undefined> {
+ if (!isCargoRunnableArgs(runnable.args)) {
+ return;
+ }
+ const runnableArgs: ra.CargoRunnableArgs = runnable.args;
+
const editor = ctx.activeRustEditor;
if (!editor) return;
@@ -119,9 +126,9 @@ async function getDebugConfiguration(
const isMultiFolderWorkspace = workspaceFolders.length > 1;
const firstWorkspace = workspaceFolders[0];
const maybeWorkspace =
- !isMultiFolderWorkspace || !runnable.args.workspaceRoot
+ !isMultiFolderWorkspace || !runnableArgs.workspaceRoot
? firstWorkspace
- : workspaceFolders.find((w) => runnable.args.workspaceRoot?.includes(w.uri.fsPath)) ||
+ : workspaceFolders.find((w) => runnableArgs.workspaceRoot?.includes(w.uri.fsPath)) ||
firstWorkspace;
const workspace = unwrapUndefinable(maybeWorkspace);
@@ -132,8 +139,8 @@ async function getDebugConfiguration(
return path.normalize(p).replace(wsFolder, "${workspaceFolder" + workspaceQualifier + "}");
}
- const env = prepareEnv(runnable, ctx.config.runnablesExtraEnv);
- const executable = await getDebugExecutable(runnable, env);
+ const env = prepareEnv(runnable.label, runnableArgs, ctx.config.runnablesExtraEnv);
+ const executable = await getDebugExecutable(runnableArgs, env);
let sourceFileMap = debugOptions.sourceFileMap;
if (sourceFileMap === "auto") {
// let's try to use the default toolchain
@@ -147,7 +154,7 @@ async function getDebugConfiguration(
}
const provider = unwrapUndefinable(knownEngines[debugEngine.id]);
- const debugConfig = provider(runnable, simplifyPath(executable), env, sourceFileMap);
+ const debugConfig = provider(runnable, runnableArgs, simplifyPath(executable), env);
if (debugConfig.type in debugOptions.engineSettings) {
const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type];
for (var key in settingsMap) {
@@ -170,11 +177,11 @@ async function getDebugConfiguration(
}
async function getDebugExecutable(
- runnable: ra.Runnable,
+ runnableArgs: ra.CargoRunnableArgs,
env: Record<string, string>,
): Promise<string> {
- const cargo = new Cargo(runnable.args.workspaceRoot || ".", debugOutput, env);
- const executable = await cargo.executableFromArgs(runnable.args.cargoArgs);
+ const cargo = new Cargo(runnableArgs.workspaceRoot || ".", debugOutput, env);
+ const executable = await cargo.executableFromArgs(runnableArgs.cargoArgs);
// if we are here, there were no compilation errors.
return executable;
@@ -182,6 +189,7 @@ async function getDebugExecutable(
function getCCppDebugConfig(
runnable: ra.Runnable,
+ runnableArgs: ra.CargoRunnableArgs,
executable: string,
env: Record<string, string>,
sourceFileMap?: Record<string, string>,
@@ -191,8 +199,8 @@ function getCCppDebugConfig(
request: "launch",
name: runnable.label,
program: executable,
- args: runnable.args.executableArgs,
- cwd: runnable.args.cwd || runnable.args.workspaceRoot || ".",
+ args: runnableArgs.executableArgs,
+ cwd: runnable.args.cwd || runnableArgs.workspaceRoot || ".",
sourceFileMap,
environment: Object.entries(env).map((entry) => ({
name: entry[0],
@@ -207,6 +215,7 @@ function getCCppDebugConfig(
function getCodeLldbDebugConfig(
runnable: ra.Runnable,
+ runnableArgs: ra.CargoRunnableArgs,
executable: string,
env: Record<string, string>,
sourceFileMap?: Record<string, string>,
@@ -216,8 +225,8 @@ function getCodeLldbDebugConfig(
request: "launch",
name: runnable.label,
program: executable,
- args: runnable.args.executableArgs,
- cwd: runnable.args.cwd || runnable.args.workspaceRoot || ".",
+ args: runnableArgs.executableArgs,
+ cwd: runnable.args.cwd || runnableArgs.workspaceRoot || ".",
sourceMap: sourceFileMap,
sourceLanguages: ["rust"],
env,
@@ -226,6 +235,7 @@ function getCodeLldbDebugConfig(
function getNativeDebugConfig(
runnable: ra.Runnable,
+ runnableArgs: ra.CargoRunnableArgs,
executable: string,
env: Record<string, string>,
_sourceFileMap?: Record<string, string>,
@@ -236,8 +246,8 @@ function getNativeDebugConfig(
name: runnable.label,
target: executable,
// See https://github.com/WebFreak001/code-debug/issues/359
- arguments: quote(runnable.args.executableArgs),
- cwd: runnable.args.cwd || runnable.args.workspaceRoot || ".",
+ arguments: quote(runnableArgs.executableArgs),
+ cwd: runnable.args.cwd || runnableArgs.workspaceRoot || ".",
env,
valuesFormatting: "prettyPrinters",
};
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 8e48aeef15..2462f06f18 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -223,17 +223,27 @@ export type OpenCargoTomlParams = {
export type Runnable = {
label: string;
location?: lc.LocationLink;
- kind: "cargo";
- args: {
- workspaceRoot?: string;
- cwd?: string;
- cargoArgs: string[];
- cargoExtraArgs: string[];
- executableArgs: string[];
- expectTest?: boolean;
- overrideCargo?: string;
- };
+ kind: "cargo" | "shell";
+ args: CargoRunnableArgs | ShellRunnableArgs;
};
+
+export type ShellRunnableArgs = {
+ kind: string;
+ program: string;
+ args: string[];
+ cwd: string;
+};
+
+export type CargoRunnableArgs = {
+ workspaceRoot?: string;
+ cargoArgs: string[];
+ cwd: string;
+ cargoExtraArgs: string[];
+ executableArgs: string[];
+ expectTest?: boolean;
+ overrideCargo?: string;
+};
+
export type RunnablesParams = {
textDocument: lc.TextDocumentIdentifier;
position: lc.Position | null;
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index 4470689cd8..52117a442a 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -9,6 +9,7 @@ import type { Config, RunnableEnvCfg, RunnableEnvCfgItem } from "./config";
import { unwrapUndefinable } from "./undefinable";
import type { LanguageClient } from "vscode-languageclient/node";
import type { RustEditor } from "./util";
+import * as toolchain from "./toolchain";
const quickPickButtons = [
{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." },
@@ -66,17 +67,23 @@ export class RunnableQuickPick implements vscode.QuickPickItem {
}
}
+export function prepareBaseEnv(): Record<string, string> {
+ const env: Record<string, string> = { RUST_BACKTRACE: "short" };
+ Object.assign(env, process.env as { [key: string]: string });
+ return env;
+}
+
export function prepareEnv(
- runnable: ra.Runnable,
+ label: string,
+ runnableArgs: ra.CargoRunnableArgs,
runnableEnvCfg: RunnableEnvCfg,
): Record<string, string> {
- const env: Record<string, string> = { RUST_BACKTRACE: "short" };
+ const env = prepareBaseEnv();
- if (runnable.args.expectTest) {
+ if (runnableArgs.expectTest) {
env["UPDATE_EXPECT"] = "1";
}
- Object.assign(env, process.env as { [key: string]: string });
const platform = process.platform;
const checkPlatform = (it: RunnableEnvCfgItem) => {
@@ -90,7 +97,7 @@ export function prepareEnv(
if (runnableEnvCfg) {
if (Array.isArray(runnableEnvCfg)) {
for (const it of runnableEnvCfg) {
- const masked = !it.mask || new RegExp(it.mask).test(runnable.label);
+ const masked = !it.mask || new RegExp(it.mask).test(label);
if (masked && checkPlatform(it)) {
Object.assign(env, it.env);
}
@@ -104,24 +111,41 @@ export function prepareEnv(
}
export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> {
- if (runnable.kind !== "cargo") {
- // rust-analyzer supports only one kind, "cargo"
- // do not use tasks.TASK_TYPE here, these are completely different meanings.
+ let definition: tasks.RustTargetDefinition;
+ if (runnable.kind === "cargo") {
+ const runnableArgs = runnable.args as ra.CargoRunnableArgs;
+ let args = createCargoArgs(runnableArgs);
+
+ let program: string;
+ if (runnableArgs.overrideCargo) {
+ // Split on spaces to allow overrides like "wrapper cargo".
+ const cargoParts = runnableArgs.overrideCargo.split(" ");
+
+ program = unwrapUndefinable(cargoParts[0]);
+ args = [...cargoParts.slice(1), ...args];
+ } else {
+ program = await toolchain.cargoPath();
+ }
- throw `Unexpected runnable kind: ${runnable.kind}`;
+ definition = {
+ type: tasks.TASK_TYPE,
+ command: program,
+ args,
+ cwd: runnableArgs.workspaceRoot || ".",
+ env: prepareEnv(runnable.label, runnableArgs, config.runnablesExtraEnv),
+ };
+ } else {
+ const runnableArgs = runnable.args as ra.ShellRunnableArgs;
+
+ definition = {
+ type: "shell",
+ command: runnableArgs.program,
+ args: runnableArgs.args,
+ cwd: runnableArgs.cwd,
+ env: prepareBaseEnv(),
+ };
}
- const args = createArgs(runnable);
-
- const definition: tasks.CargoTaskDefinition = {
- type: tasks.TASK_TYPE,
- command: unwrapUndefinable(args[0]), // run, test, etc...
- args: args.slice(1),
- cwd: runnable.args.workspaceRoot || ".",
- env: prepareEnv(runnable, config.runnablesExtraEnv),
- overrideCargo: runnable.args.overrideCargo,
- };
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate()
const task = await tasks.buildRustTask(
@@ -141,13 +165,13 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
return task;
}
-export function createArgs(runnable: ra.Runnable): string[] {
- const args = [...runnable.args.cargoArgs]; // should be a copy!
- if (runnable.args.cargoExtraArgs) {
- args.push(...runnable.args.cargoExtraArgs); // Append user-specified cargo options.
+export function createCargoArgs(runnableArgs: ra.CargoRunnableArgs): string[] {
+ const args = [...runnableArgs.cargoArgs]; // should be a copy!
+ if (runnableArgs.cargoExtraArgs) {
+ args.push(...runnableArgs.cargoExtraArgs); // Append user-specified cargo options.
}
- if (runnable.args.executableArgs.length > 0) {
- args.push("--", ...runnable.args.executableArgs);
+ if (runnableArgs.executableArgs.length > 0) {
+ args.push("--", ...runnableArgs.executableArgs);
}
return args;
}
diff --git a/editors/code/src/tasks.ts b/editors/code/src/tasks.ts
index 2b3abc5d65..c28a919231 100644
--- a/editors/code/src/tasks.ts
+++ b/editors/code/src/tasks.ts
@@ -10,7 +10,7 @@ export const TASK_TYPE = "cargo";
export const TASK_SOURCE = "rust";
-export interface CargoTaskDefinition extends vscode.TaskDefinition {
+export interface RustTargetDefinition extends vscode.TaskDefinition {
// The cargo command, such as "run" or "check".
command: string;
// Additional arguments passed to the cargo command.
@@ -69,7 +69,7 @@ class RustTaskProvider implements vscode.TaskProvider {
// we need to inform VSCode how to execute that command by creating
// a ShellExecution for it.
- const definition = task.definition as CargoTaskDefinition;
+ const definition = task.definition as RustTargetDefinition;
if (definition.type === TASK_TYPE) {
return await buildRustTask(
@@ -87,7 +87,7 @@ class RustTaskProvider implements vscode.TaskProvider {
export async function buildRustTask(
scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
- definition: CargoTaskDefinition,
+ definition: RustTargetDefinition,
name: string,
problemMatcher: string[],
customRunner?: string,
@@ -108,7 +108,7 @@ export async function buildRustTask(
}
async function cargoToExecution(
- definition: CargoTaskDefinition,
+ definition: RustTargetDefinition,
customRunner: string | undefined,
throwOnError: boolean,
): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
@@ -138,20 +138,31 @@ async function cargoToExecution(
}
}
- // Check whether we must use a user-defined substitute for cargo.
- // Split on spaces to allow overrides like "wrapper cargo".
- const cargoPath = await toolchain.cargoPath();
- const cargoCommand = definition.overrideCargo?.split(" ") ?? [cargoPath];
-
- const args = [definition.command].concat(definition.args ?? []);
- const fullCommand = [...cargoCommand, ...args];
-
- const processName = unwrapUndefinable(fullCommand[0]);
-
- return new vscode.ProcessExecution(processName, fullCommand.slice(1), {
- cwd: definition.cwd,
- env: definition.env,
- });
+ // this is a cargo task; do Cargo-esque processing
+ if (definition.type === TASK_TYPE) {
+ // Check whether we must use a user-defined substitute for cargo.
+ // Split on spaces to allow overrides like "wrapper cargo".
+ const cargoPath = await toolchain.cargoPath();
+ const cargoCommand = definition.overrideCargo?.split(" ") ?? [cargoPath];
+
+ const args = [definition.command].concat(definition.args ?? []);
+ const fullCommand = [...cargoCommand, ...args];
+ const processName = unwrapUndefinable(fullCommand[0]);
+
+ return new vscode.ProcessExecution(processName, fullCommand.slice(1), {
+ cwd: definition.cwd,
+ env: definition.env,
+ });
+ } else {
+ // we've been handed a process definition by rust-analyzer, trust all its inputs
+ // and make a shell execution.
+ const args = unwrapUndefinable(definition.args);
+
+ return new vscode.ProcessExecution(definition.command, args, {
+ cwd: definition.cwd,
+ env: definition.env,
+ });
+ }
}
export function activateTaskProvider(config: Config): vscode.Disposable {
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 51f921a296..868cb2b780 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -2,6 +2,7 @@ import * as vscode from "vscode";
import { strict as nativeAssert } from "assert";
import { exec, type ExecOptions, spawnSync } from "child_process";
import { inspect } from "util";
+import type { CargoRunnableArgs, ShellRunnableArgs } from "./lsp_ext";
import type { Env } from "./client";
export function assert(condition: boolean, explanation: string): asserts condition {
@@ -77,6 +78,12 @@ export function isCargoTomlDocument(document: vscode.TextDocument): document is
return document.uri.scheme === "file" && document.fileName.endsWith("Cargo.toml");
}
+export function isCargoRunnableArgs(
+ args: CargoRunnableArgs | ShellRunnableArgs,
+): args is CargoRunnableArgs {
+ return (args as CargoRunnableArgs).executableArgs !== undefined;
+}
+
export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
return isRustDocument(editor.document);
}
diff --git a/editors/code/tests/unit/runnable_env.test.ts b/editors/code/tests/unit/runnable_env.test.ts
index b1407ce019..21bdaf5384 100644
--- a/editors/code/tests/unit/runnable_env.test.ts
+++ b/editors/code/tests/unit/runnable_env.test.ts
@@ -10,6 +10,7 @@ function makeRunnable(label: string): ra.Runnable {
kind: "cargo",
args: {
cargoArgs: [],
+ cwd: ".",
executableArgs: [],
cargoExtraArgs: [],
},
@@ -18,7 +19,8 @@ function makeRunnable(label: string): ra.Runnable {
function fakePrepareEnv(runnableName: string, config: RunnableEnvCfg): Record<string, string> {
const runnable = makeRunnable(runnableName);
- return prepareEnv(runnable, config);
+ const runnableArgs = runnable.args as ra.CargoRunnableArgs;
+ return prepareEnv(runnable.label, runnableArgs, config);
}
export async function getTests(ctx: Context) {