Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | crates/project-model/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/project-model/src/project_json.rs | 177 | ||||
| -rw-r--r-- | crates/project-model/src/workspace.rs | 2 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/global_state.rs | 61 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/handlers/request.rs | 70 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lsp/ext.rs | 27 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lsp/to_proto.rs | 184 | ||||
| -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.md | 15 | ||||
| -rw-r--r-- | editors/code/src/commands.ts | 7 | ||||
| -rw-r--r-- | editors/code/src/debug.ts | 40 | ||||
| -rw-r--r-- | editors/code/src/lsp_ext.ts | 30 | ||||
| -rw-r--r-- | editors/code/src/run.ts | 76 | ||||
| -rw-r--r-- | editors/code/src/tasks.ts | 47 | ||||
| -rw-r--r-- | editors/code/src/util.ts | 7 | ||||
| -rw-r--r-- | editors/code/tests/unit/runnable_env.test.ts | 4 |
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, ¶ms.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, ¶ms.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, ¶ms.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) { |