feature: add build system info; runnables to rust-project.json

This commit is contained in:
Wilfred Hughes 2024-06-11 11:55:04 -04:00 committed by David Barsky
parent 14a1f4530c
commit 71a78a9cdc
17 changed files with 628 additions and 227 deletions

View file

@ -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;

View file

@ -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`

View file

@ -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.

View file

@ -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 {

View file

@ -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());

View file

@ -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;

View file

@ -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 {

View file

@ -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(),
})
}

View file

@ -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);

View file

@ -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 }`

View file

@ -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.");

View file

@ -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",
};

View file

@ -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;

View file

@ -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);
throw `Unexpected runnable kind: ${runnable.kind}`;
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();
}
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;
}

View file

@ -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];
// 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 args = [definition.command].concat(definition.args ?? []);
const fullCommand = [...cargoCommand, ...args];
const processName = unwrapUndefinable(fullCommand[0]);
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(processName, fullCommand.slice(1), {
cwd: definition.cwd,
env: definition.env,
});
return new vscode.ProcessExecution(definition.command, args, {
cwd: definition.cwd,
env: definition.env,
});
}
}
export function activateTaskProvider(config: Config): vscode.Disposable {

View file

@ -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);
}

View file

@ -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) {