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 cfg;
mod env; mod env;
mod manifest_path; mod manifest_path;
mod project_json; pub mod project_json;
mod rustc_cfg; mod rustc_cfg;
mod sysroot; mod sysroot;
pub mod target_data_layout; pub mod target_data_layout;

View file

@ -33,7 +33,7 @@
//! //!
//! * file on disk //! * file on disk
//! * a field in the config (ie, you can send a JSON request with the contents //! * 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, //! 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 //! 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 serde::{de, Deserialize, Serialize};
use span::Edition; use span::Edition;
use crate::cfg::CfgFlag; use crate::{cfg::CfgFlag, ManifestPath, TargetKind};
use crate::ManifestPath;
/// Roots and crates that compose this Rust project. /// Roots and crates that compose this Rust project.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -68,6 +67,10 @@ pub struct ProjectJson {
project_root: AbsPathBuf, project_root: AbsPathBuf,
manifest: Option<ManifestPath>, manifest: Option<ManifestPath>,
crates: Vec<Crate>, 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 /// 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) exclude: Vec<AbsPathBuf>,
pub(crate) is_proc_macro: bool, pub(crate) is_proc_macro: bool,
pub(crate) repository: Option<String>, 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 { impl ProjectJson {
@ -95,6 +178,7 @@ impl ProjectJson {
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `manifest` - The path to the `rust-project.json`.
/// * `base` - The path to the workspace root (i.e. the folder containing `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 /// * `data` - The parsed contents of `rust-project.json`, or project json that's passed via
/// configuration. /// configuration.
@ -109,6 +193,7 @@ impl ProjectJson {
sysroot_src: data.sysroot_src.map(absolutize_on_base), sysroot_src: data.sysroot_src.map(absolutize_on_base),
project_root: base.to_path_buf(), project_root: base.to_path_buf(),
manifest, manifest,
runnables: data.runnables.into_iter().map(Runnable::from).collect(),
crates: data crates: data
.crates .crates
.into_iter() .into_iter()
@ -127,6 +212,15 @@ impl ProjectJson {
None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()), 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 { Crate {
display_name: crate_data display_name: crate_data
.display_name .display_name
@ -146,6 +240,7 @@ impl ProjectJson {
exclude, exclude,
is_proc_macro: crate_data.is_proc_macro, is_proc_macro: crate_data.is_proc_macro,
repository: crate_data.repository, repository: crate_data.repository,
build,
} }
}) })
.collect(), .collect(),
@ -167,7 +262,15 @@ impl ProjectJson {
&self.project_root &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> { pub fn manifest(&self) -> Option<&ManifestPath> {
self.manifest.as_ref() self.manifest.as_ref()
} }
@ -176,6 +279,10 @@ impl ProjectJson {
pub fn manifest_or_root(&self) -> &AbsPath { pub fn manifest_or_root(&self) -> &AbsPath {
self.manifest.as_ref().map_or(&self.project_root, |manifest| manifest.as_ref()) 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)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -183,6 +290,8 @@ pub struct ProjectJsonData {
sysroot: Option<Utf8PathBuf>, sysroot: Option<Utf8PathBuf>,
sysroot_src: Option<Utf8PathBuf>, sysroot_src: Option<Utf8PathBuf>,
crates: Vec<CrateData>, crates: Vec<CrateData>,
#[serde(default)]
runnables: Vec<RunnableData>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -205,6 +314,8 @@ struct CrateData {
is_proc_macro: bool, is_proc_macro: bool,
#[serde(default)] #[serde(default)]
repository: Option<String>, repository: Option<String>,
#[serde(default)]
build: Option<BuildData>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -220,6 +331,48 @@ enum EditionData {
Edition2024, 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 { impl From<EditionData> for Edition {
fn from(data: EditionData) -> Self { fn from(data: EditionData) -> Self {
match data { 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. /// Identifies a crate by position in the crates array.
/// ///
/// This will differ from `CrateId` when multiple `ProjectJson` /// 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. /// Environment variables set in the `.cargo/config` file.
cargo_config_extra_env: FxHashMap<String, String>, 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), Json(ProjectJson),
// FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning. // 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. // That's not the end user experience we should strive for.

View file

@ -18,10 +18,7 @@ use parking_lot::{
RwLockWriteGuard, RwLockWriteGuard,
}; };
use proc_macro_api::ProcMacroServer; use proc_macro_api::ProcMacroServer;
use project_model::{ use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
CargoWorkspace, ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, Target,
WorkspaceBuildScripts,
};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{span, Level}; use tracing::{span, Level};
use triomphe::Arc; use triomphe::Arc;
@ -40,6 +37,7 @@ use crate::{
mem_docs::MemDocs, mem_docs::MemDocs,
op_queue::OpQueue, op_queue::OpQueue,
reload, reload,
target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec},
task_pool::{TaskPool, TaskQueue}, task_pool::{TaskPool, TaskQueue},
}; };
@ -556,21 +554,52 @@ impl GlobalStateSnapshot {
self.vfs_read().file_path(file_id).clone() self.vfs_read().file_path(file_id).clone()
} }
pub(crate) fn cargo_target_for_crate_root( pub(crate) fn target_spec_for_crate(&self, crate_id: CrateId) -> Option<TargetSpec> {
&self,
crate_id: CrateId,
) -> Option<(&CargoWorkspace, Target)> {
let file_id = self.analysis.crate_root(crate_id).ok()?; let file_id = self.analysis.crate_root(crate_id).ok()?;
let path = self.vfs_read().file_path(file_id).clone(); let path = self.vfs_read().file_path(file_id).clone();
let path = path.as_path()?; let path = path.as_path()?;
self.workspaces.iter().find_map(|ws| match &ws.kind {
ProjectWorkspaceKind::Cargo { cargo, .. } for workspace in self.workspaces.iter() {
| ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => { match &workspace.kind {
cargo.target_by_root(path).map(|it| (cargo, it)) ProjectWorkspaceKind::Cargo { cargo, .. }
} | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
ProjectWorkspaceKind::Json { .. } => None, let Some(target_idx) = cargo.target_by_root(path) else {
ProjectWorkspaceKind::DetachedFile { .. } => None, 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 { 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 vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
use crate::{ use crate::{
cargo_target_spec::CargoTargetSpec,
config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
diff::diff, diff::diff,
global_state::{GlobalState, GlobalStateSnapshot}, global_state::{GlobalState, GlobalStateSnapshot},
@ -51,6 +50,7 @@ use crate::{
self, CrateInfoResult, ExternalDocsPair, ExternalDocsResponse, FetchDependencyListParams, self, CrateInfoResult, ExternalDocsPair, ExternalDocsResponse, FetchDependencyListParams,
FetchDependencyListResult, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams, FetchDependencyListResult, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams,
}, },
target_spec::TargetSpec,
}; };
pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> anyhow::Result<()> { 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, Some(&crate_id) => crate_id,
None => return Ok(None), None => return Ok(None),
}; };
let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? { let cargo_spec = match TargetSpec::for_file(&snap, file_id)? {
Some(it) => it, Some(TargetSpec::Cargo(it)) => it,
None => return Ok(None), Some(TargetSpec::ProjectJson(_)) | None => return Ok(None),
}; };
if snap.analysis.crate_root(crate_id)? == file_id { 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 file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.file_line_index(file_id)?; let line_index = snap.file_line_index(file_id)?;
let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok()); 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 { let expect_test = match offset {
Some(offset) => { Some(offset) => {
@ -840,21 +840,24 @@ pub(crate) fn handle_runnables(
if should_skip_for_offset(&runnable, offset) { if should_skip_for_offset(&runnable, offset) {
continue; continue;
} }
if should_skip_target(&runnable, cargo_spec.as_ref()) { if should_skip_target(&runnable, target_spec.as_ref()) {
continue; continue;
} }
let mut runnable = to_proto::runnable(&snap, runnable)?; if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? {
if expect_test { if expect_test {
runnable.label = format!("{} + expect", runnable.label); if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args {
runnable.args.expect_test = Some(true); 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 // Add `cargo check` and `cargo test` for all targets of the whole package
let config = snap.config.runnables(); let config = snap.config.runnables();
match cargo_spec { match target_spec {
Some(spec) => { Some(TargetSpec::Cargo(spec)) => {
let is_crate_no_std = snap.analysis.is_crate_no_std(spec.crate_id)?; let is_crate_no_std = snap.analysis.is_crate_no_std(spec.crate_id)?;
for cmd in ["check", "run", "test"] { for cmd in ["check", "run", "test"] {
if cmd == "run" && spec.target_kind != TargetKind::Bin { if cmd == "run" && spec.target_kind != TargetKind::Bin {
@ -879,7 +882,7 @@ pub(crate) fn handle_runnables(
), ),
location: None, location: None,
kind: lsp_ext::RunnableKind::Cargo, 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()), workspace_root: Some(spec.workspace_root.clone().into()),
cwd: Some(cwd.into()), cwd: Some(cwd.into()),
override_cargo: config.override_cargo.clone(), override_cargo: config.override_cargo.clone(),
@ -887,17 +890,18 @@ pub(crate) fn handle_runnables(
cargo_extra_args: config.cargo_extra_args.clone(), cargo_extra_args: config.cargo_extra_args.clone(),
executable_args: Vec::new(), executable_args: Vec::new(),
expect_test: None, expect_test: None,
}, }),
}) })
} }
} }
Some(TargetSpec::ProjectJson(_)) => {}
None => { None => {
if !snap.config.linked_or_discovered_projects().is_empty() { if !snap.config.linked_or_discovered_projects().is_empty() {
res.push(lsp_ext::Runnable { res.push(lsp_ext::Runnable {
label: "cargo check --workspace".to_owned(), label: "cargo check --workspace".to_owned(),
location: None, location: None,
kind: lsp_ext::RunnableKind::Cargo, kind: lsp_ext::RunnableKind::Cargo,
args: lsp_ext::CargoRunnable { args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs {
workspace_root: None, workspace_root: None,
cwd: None, cwd: None,
override_cargo: config.override_cargo, override_cargo: config.override_cargo,
@ -905,7 +909,7 @@ pub(crate) fn handle_runnables(
cargo_extra_args: config.cargo_extra_args, cargo_extra_args: config.cargo_extra_args,
executable_args: Vec::new(), executable_args: Vec::new(),
expect_test: None, expect_test: None,
}, }),
}); });
} }
} }
@ -931,7 +935,7 @@ pub(crate) fn handle_related_tests(
let tests = snap.analysis.related_tests(position, None)?; let tests = snap.analysis.related_tests(position, None)?;
let mut res = Vec::new(); let mut res = Vec::new();
for it in tests { 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 }) 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 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( let annotations = snap.analysis.annotations(
&AnnotationConfig { &AnnotationConfig {
binary_target: cargo_target_spec binary_target: target_spec
.map(|spec| { .map(|spec| {
matches!( matches!(
spec.target_kind, spec.target_kind(),
TargetKind::Bin | TargetKind::Example | TargetKind::Test 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 _p = tracing::info_span!("handle_open_cargo_toml").entered();
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?; let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? { let cargo_spec = match TargetSpec::for_file(&snap, file_id)? {
Some(it) => it, Some(TargetSpec::Cargo(it)) => it,
None => return Ok(None), Some(TargetSpec::ProjectJson(_)) | None => return Ok(None),
}; };
let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml); let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml);
@ -1954,8 +1958,8 @@ fn runnable_action_links(
return None; return None;
} }
let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?; let target_spec = TargetSpec::for_file(snap, runnable.nav.file_id).ok()?;
if should_skip_target(&runnable, cargo_spec.as_ref()) { if should_skip_target(&runnable, target_spec.as_ref()) {
return None; return None;
} }
@ -1965,7 +1969,7 @@ fn runnable_action_links(
} }
let title = runnable.title(); 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(); let mut group = lsp_ext::CommandLinkGroup::default();
@ -2020,13 +2024,13 @@ fn prepare_hover_actions(
.collect() .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 { match runnable.kind {
RunnableKind::Bin => { RunnableKind::Bin => {
// Do not suggest binary run on other target than binary // Do not suggest binary run on other target than binary
match &cargo_spec { match &cargo_spec {
Some(spec) => !matches!( Some(spec) => !matches!(
spec.target_kind, spec.target_kind(),
TargetKind::Bin | TargetKind::Example | TargetKind::Test TargetKind::Bin | TargetKind::Example | TargetKind::Test
), ),
None => true, None => true,
@ -2103,9 +2107,9 @@ fn run_rustfmt(
} }
RustfmtConfig::CustomCommand { command, args } => { RustfmtConfig::CustomCommand { command, args } => {
let cmd = Utf8PathBuf::from(&command); let cmd = Utf8PathBuf::from(&command);
let workspace = CargoTargetSpec::for_file(snap, file_id)?; let target_spec = TargetSpec::for_file(snap, file_id)?;
let mut cmd = match workspace { let mut cmd = match target_spec {
Some(spec) => { Some(TargetSpec::Cargo(spec)) => {
// approach: if the command name contains a path separator, join it with the workspace root. // 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. // however, if the path is absolute, joining will result in the absolute path being preserved.
// as a fallback, rely on $PATH-based discovery. // as a fallback, rely on $PATH-based discovery.
@ -2118,7 +2122,7 @@ fn run_rustfmt(
}; };
process::Command::new(cmd_path) process::Command::new(cmd_path)
} }
None => process::Command::new(cmd), _ => process::Command::new(cmd),
}; };
cmd.envs(snap.config.extra_env()); cmd.envs(snap.config.extra_env());

View file

@ -14,7 +14,6 @@
pub mod cli; pub mod cli;
mod caps; mod caps;
mod cargo_target_spec;
mod diagnostics; mod diagnostics;
mod diff; mod diff;
mod dispatch; mod dispatch;
@ -24,6 +23,7 @@ mod main_loop;
mod mem_docs; mod mem_docs;
mod op_queue; mod op_queue;
mod reload; mod reload;
mod target_spec;
mod task_pool; mod task_pool;
mod version; mod version;

View file

@ -3,7 +3,6 @@
#![allow(clippy::disallowed_types)] #![allow(clippy::disallowed_types)]
use std::ops; use std::ops;
use std::path::PathBuf;
use ide_db::line_index::WideEncoding; use ide_db::line_index::WideEncoding;
use lsp_types::request::Request; use lsp_types::request::Request;
@ -12,6 +11,7 @@ use lsp_types::{
PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams, PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
}; };
use lsp_types::{PositionEncodingKind, Url}; use lsp_types::{PositionEncodingKind, Url};
use paths::Utf8PathBuf;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -439,24 +439,33 @@ pub struct Runnable {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<lsp_types::LocationLink>, pub location: Option<lsp_types::LocationLink>,
pub kind: RunnableKind, 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)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum RunnableKind { pub enum RunnableKind {
Cargo, Cargo,
Shell,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CargoRunnable { pub struct CargoRunnableArgs {
// command to be executed instead of cargo // command to be executed instead of cargo
pub override_cargo: Option<String>, pub override_cargo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub workspace_root: Option<PathBuf>, pub workspace_root: Option<Utf8PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathBuf>, pub cwd: Option<Utf8PathBuf>,
// command, --package and --lib stuff // command, --package and --lib stuff
pub cargo_args: Vec<String>, pub cargo_args: Vec<String>,
// user-specified additional cargo args, like `--release`. // user-specified additional cargo args, like `--release`.
@ -467,6 +476,14 @@ pub struct CargoRunnable {
pub expect_test: Option<bool>, 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 {} pub enum RelatedTests {}
impl Request for RelatedTests { impl Request for RelatedTests {

View file

@ -21,16 +21,17 @@ use serde_json::to_value;
use vfs::AbsPath; use vfs::AbsPath;
use crate::{ use crate::{
cargo_target_spec::CargoTargetSpec,
config::{CallInfoConfig, Config}, config::{CallInfoConfig, Config},
global_state::GlobalStateSnapshot, global_state::GlobalStateSnapshot,
line_index::{LineEndings, LineIndex, PositionEncoding}, line_index::{LineEndings, LineIndex, PositionEncoding},
lsp::{ lsp::{
ext::ShellRunnableArgs,
semantic_tokens::{self, standard_fallback_type}, semantic_tokens::{self, standard_fallback_type},
utils::invalid_params_error, utils::invalid_params_error,
LspError, LspError,
}, },
lsp_ext::{self, SnippetTextEdit}, lsp_ext::{self, SnippetTextEdit},
target_spec::{CargoTargetSpec, TargetSpec},
}; };
pub(crate) fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position { 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( pub(crate) fn runnable(
snap: &GlobalStateSnapshot, snap: &GlobalStateSnapshot,
runnable: Runnable, runnable: Runnable,
) -> Cancellable<lsp_ext::Runnable> { ) -> Cancellable<Option<lsp_ext::Runnable>> {
let config = snap.config.runnables(); let config = snap.config.runnables();
let spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id)?; let target_spec = TargetSpec::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)?;
Ok(lsp_ext::Runnable { match target_spec {
label, Some(TargetSpec::Cargo(spec)) => {
location: Some(location), let workspace_root = spec.workspace_root.clone();
kind: lsp_ext::RunnableKind::Cargo,
args: lsp_ext::CargoRunnable { let target = spec.target.clone();
workspace_root: workspace_root.map(|it| it.into()),
cwd, let (cargo_args, executable_args) = CargoTargetSpec::runnable_args(
override_cargo: config.override_cargo, snap,
cargo_args, Some(spec.clone()),
cargo_extra_args: config.cargo_extra_args, &runnable.kind,
executable_args, &runnable.cfg,
expect_test: None, );
},
}) 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( pub(crate) fn code_lens(
@ -1407,33 +1464,37 @@ pub(crate) fn code_lens(
}; };
let r = runnable(snap, run)?; let r = runnable(snap, run)?;
let lens_config = snap.config.lens(); if let Some(r) = r {
if lens_config.run let has_root = match &r.args {
&& client_commands_config.run_single lsp_ext::RunnableArgs::Cargo(c) => c.workspace_root.is_some(),
&& r.args.workspace_root.is_some() lsp_ext::RunnableArgs::Shell(_) => true,
{ };
let command = command::run_single(&r, &title);
acc.push(lsp_types::CodeLens { let lens_config = snap.config.lens();
range: annotation_range, if lens_config.run && client_commands_config.run_single && has_root {
command: Some(command), let command = command::run_single(&r, &title);
data: None, acc.push(lsp_types::CodeLens {
}) range: annotation_range,
} command: Some(command),
if lens_config.debug && can_debug && client_commands_config.debug_single { data: None,
let command = command::debug_single(&r); })
acc.push(lsp_types::CodeLens { }
range: annotation_range, if lens_config.debug && can_debug && client_commands_config.debug_single {
command: Some(command), let command = command::debug_single(&r);
data: None, acc.push(lsp_types::CodeLens {
}) range: annotation_range,
} command: Some(command),
if lens_config.interpret { data: None,
let command = command::interpret_single(&r); })
acc.push(lsp_types::CodeLens { }
range: annotation_range, if lens_config.interpret {
command: Some(command), let command = command::interpret_single(&r);
data: None, acc.push(lsp_types::CodeLens {
}) range: annotation_range,
command: Some(command),
data: None,
})
}
} }
} }
AnnotationKind::HasImpls { pos, data } => { AnnotationKind::HasImpls { pos, data } => {
@ -1538,12 +1599,8 @@ pub(crate) fn test_item(
id: test_item.id, id: test_item.id,
label: test_item.label, label: test_item.label,
kind: match test_item.kind { kind: match test_item.kind {
ide::TestItemKind::Crate(id) => 'b: { ide::TestItemKind::Crate(id) => match snap.target_spec_for_crate(id) {
let Some((cargo_ws, target)) = snap.cargo_target_for_crate_root(id) else { Some(target_spec) => match target_spec.target_kind() {
break 'b lsp_ext::TestItemKind::Package;
};
let target = &cargo_ws[target];
match target.kind {
project_model::TargetKind::Bin project_model::TargetKind::Bin
| project_model::TargetKind::Lib { .. } | project_model::TargetKind::Lib { .. }
| project_model::TargetKind::Example | project_model::TargetKind::Example
@ -1552,8 +1609,9 @@ pub(crate) fn test_item(
project_model::TargetKind::Test => lsp_ext::TestItemKind::Test, project_model::TargetKind::Test => lsp_ext::TestItemKind::Test,
// benches are not tests needed to be shown in the test explorer // benches are not tests needed to be shown in the test explorer
project_model::TargetKind::Bench => return None, project_model::TargetKind::Bench => return None,
} },
} None => lsp_ext::TestItemKind::Package,
},
ide::TestItemKind::Module => lsp_ext::TestItemKind::Module, ide::TestItemKind::Module => lsp_ext::TestItemKind::Module,
ide::TestItemKind::Function => lsp_ext::TestItemKind::Test, ide::TestItemKind::Function => lsp_ext::TestItemKind::Test,
}, },
@ -1566,7 +1624,7 @@ pub(crate) fn test_item(
.file .file
.map(|f| lsp_types::TextDocumentIdentifier { uri: url(snap, f) }), .map(|f| lsp_types::TextDocumentIdentifier { uri: url(snap, f) }),
range: line_index.and_then(|l| Some(range(l, test_item.text_range?))), 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 std::mem;
use cfg::{CfgAtom, CfgExpr}; use cfg::{CfgAtom, CfgExpr};
use ide::{Cancellable, CrateId, FileId, RunnableKind, TestId}; use ide::{Cancellable, CrateId, FileId, RunnableKind, TestId};
use project_model::project_json::Runnable;
use project_model::{CargoFeatures, ManifestPath, TargetKind}; use project_model::{CargoFeatures, ManifestPath, TargetKind};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use vfs::AbsPathBuf; use vfs::AbsPathBuf;
use crate::global_state::GlobalStateSnapshot; 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. /// Abstract representation of Cargo target.
/// ///
/// We use it to cook up the set of cli args we need to pass to Cargo to /// We use it to cook up the set of cli args we need to pass to Cargo to
/// build/test/run the target. /// build/test/run the target.
#[derive(Clone)] #[derive(Clone, Debug)]
pub(crate) struct CargoTargetSpec { pub(crate) struct CargoTargetSpec {
pub(crate) workspace_root: AbsPathBuf, pub(crate) workspace_root: AbsPathBuf,
pub(crate) cargo_toml: ManifestPath, pub(crate) cargo_toml: ManifestPath,
@ -26,6 +58,51 @@ pub(crate) struct CargoTargetSpec {
pub(crate) features: FxHashSet<String>, 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 { impl CargoTargetSpec {
pub(crate) fn runnable_args( pub(crate) fn runnable_args(
snap: &GlobalStateSnapshot, snap: &GlobalStateSnapshot,
@ -122,35 +199,6 @@ impl CargoTargetSpec {
(cargo_args, executable_args) (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) { pub(crate) fn push_to(self, buf: &mut Vec<String>, kind: &RunnableKind) {
buf.push("--package".to_owned()); buf.push("--package".to_owned());
buf.push(self.package); 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 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: 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 ```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 ## Test explorer
**Experimental Client Capability:** `{ "testExplorer": boolean }` **Experimental Client Capability:** `{ "testExplorer": boolean }`

View file

@ -9,10 +9,11 @@ import {
applySnippetTextEdits, applySnippetTextEdits,
type SnippetTextDocumentEdit, type SnippetTextDocumentEdit,
} from "./snippets"; } from "./snippets";
import { type RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run"; import { type RunnableQuickPick, selectRunnable, createTask, createCargoArgs } from "./run";
import { AstInspector } from "./ast_inspector"; import { AstInspector } from "./ast_inspector";
import { import {
isRustDocument, isRustDocument,
isCargoRunnableArgs,
isCargoTomlDocument, isCargoTomlDocument,
sleep, sleep,
isRustEditor, isRustEditor,
@ -1154,8 +1155,8 @@ export function copyRunCommandLine(ctx: CtxInit) {
let prevRunnable: RunnableQuickPick | undefined; let prevRunnable: RunnableQuickPick | undefined;
return async () => { return async () => {
const item = await selectRunnable(ctx, prevRunnable); const item = await selectRunnable(ctx, prevRunnable);
if (!item) return; if (!item || !isCargoRunnableArgs(item.runnable.args)) return;
const args = createArgs(item.runnable); const args = createCargoArgs(item.runnable.args);
const commandLine = ["cargo", ...args].join(" "); const commandLine = ["cargo", ...args].join(" ");
await vscode.env.clipboard.writeText(commandLine); await vscode.env.clipboard.writeText(commandLine);
await vscode.window.showInformationMessage("Cargo invocation copied to the clipboard."); 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 type { Ctx } from "./ctx";
import { prepareEnv } from "./run"; import { prepareEnv } from "./run";
import { unwrapUndefinable } from "./undefinable"; import { unwrapUndefinable } from "./undefinable";
import { isCargoRunnableArgs } from "./util";
const debugOutput = vscode.window.createOutputChannel("Debug"); const debugOutput = vscode.window.createOutputChannel("Debug");
type DebugConfigProvider = ( type DebugConfigProvider = (
config: ra.Runnable, runnable: ra.Runnable,
runnableArgs: ra.CargoRunnableArgs,
executable: string, executable: string,
env: Record<string, string>, env: Record<string, string>,
sourceFileMap?: Record<string, string>, sourceFileMap?: Record<string, string>,
@ -76,6 +78,11 @@ async function getDebugConfiguration(
ctx: Ctx, ctx: Ctx,
runnable: ra.Runnable, runnable: ra.Runnable,
): Promise<vscode.DebugConfiguration | undefined> { ): Promise<vscode.DebugConfiguration | undefined> {
if (!isCargoRunnableArgs(runnable.args)) {
return;
}
const runnableArgs: ra.CargoRunnableArgs = runnable.args;
const editor = ctx.activeRustEditor; const editor = ctx.activeRustEditor;
if (!editor) return; if (!editor) return;
@ -119,9 +126,9 @@ async function getDebugConfiguration(
const isMultiFolderWorkspace = workspaceFolders.length > 1; const isMultiFolderWorkspace = workspaceFolders.length > 1;
const firstWorkspace = workspaceFolders[0]; const firstWorkspace = workspaceFolders[0];
const maybeWorkspace = const maybeWorkspace =
!isMultiFolderWorkspace || !runnable.args.workspaceRoot !isMultiFolderWorkspace || !runnableArgs.workspaceRoot
? firstWorkspace ? firstWorkspace
: workspaceFolders.find((w) => runnable.args.workspaceRoot?.includes(w.uri.fsPath)) || : workspaceFolders.find((w) => runnableArgs.workspaceRoot?.includes(w.uri.fsPath)) ||
firstWorkspace; firstWorkspace;
const workspace = unwrapUndefinable(maybeWorkspace); const workspace = unwrapUndefinable(maybeWorkspace);
@ -132,8 +139,8 @@ async function getDebugConfiguration(
return path.normalize(p).replace(wsFolder, "${workspaceFolder" + workspaceQualifier + "}"); return path.normalize(p).replace(wsFolder, "${workspaceFolder" + workspaceQualifier + "}");
} }
const env = prepareEnv(runnable, ctx.config.runnablesExtraEnv); const env = prepareEnv(runnable.label, runnableArgs, ctx.config.runnablesExtraEnv);
const executable = await getDebugExecutable(runnable, env); const executable = await getDebugExecutable(runnableArgs, env);
let sourceFileMap = debugOptions.sourceFileMap; let sourceFileMap = debugOptions.sourceFileMap;
if (sourceFileMap === "auto") { if (sourceFileMap === "auto") {
// let's try to use the default toolchain // let's try to use the default toolchain
@ -147,7 +154,7 @@ async function getDebugConfiguration(
} }
const provider = unwrapUndefinable(knownEngines[debugEngine.id]); 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) { if (debugConfig.type in debugOptions.engineSettings) {
const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type];
for (var key in settingsMap) { for (var key in settingsMap) {
@ -170,11 +177,11 @@ async function getDebugConfiguration(
} }
async function getDebugExecutable( async function getDebugExecutable(
runnable: ra.Runnable, runnableArgs: ra.CargoRunnableArgs,
env: Record<string, string>, env: Record<string, string>,
): Promise<string> { ): Promise<string> {
const cargo = new Cargo(runnable.args.workspaceRoot || ".", debugOutput, env); const cargo = new Cargo(runnableArgs.workspaceRoot || ".", debugOutput, env);
const executable = await cargo.executableFromArgs(runnable.args.cargoArgs); const executable = await cargo.executableFromArgs(runnableArgs.cargoArgs);
// if we are here, there were no compilation errors. // if we are here, there were no compilation errors.
return executable; return executable;
@ -182,6 +189,7 @@ async function getDebugExecutable(
function getCCppDebugConfig( function getCCppDebugConfig(
runnable: ra.Runnable, runnable: ra.Runnable,
runnableArgs: ra.CargoRunnableArgs,
executable: string, executable: string,
env: Record<string, string>, env: Record<string, string>,
sourceFileMap?: Record<string, string>, sourceFileMap?: Record<string, string>,
@ -191,8 +199,8 @@ function getCCppDebugConfig(
request: "launch", request: "launch",
name: runnable.label, name: runnable.label,
program: executable, program: executable,
args: runnable.args.executableArgs, args: runnableArgs.executableArgs,
cwd: runnable.args.cwd || runnable.args.workspaceRoot || ".", cwd: runnable.args.cwd || runnableArgs.workspaceRoot || ".",
sourceFileMap, sourceFileMap,
environment: Object.entries(env).map((entry) => ({ environment: Object.entries(env).map((entry) => ({
name: entry[0], name: entry[0],
@ -207,6 +215,7 @@ function getCCppDebugConfig(
function getCodeLldbDebugConfig( function getCodeLldbDebugConfig(
runnable: ra.Runnable, runnable: ra.Runnable,
runnableArgs: ra.CargoRunnableArgs,
executable: string, executable: string,
env: Record<string, string>, env: Record<string, string>,
sourceFileMap?: Record<string, string>, sourceFileMap?: Record<string, string>,
@ -216,8 +225,8 @@ function getCodeLldbDebugConfig(
request: "launch", request: "launch",
name: runnable.label, name: runnable.label,
program: executable, program: executable,
args: runnable.args.executableArgs, args: runnableArgs.executableArgs,
cwd: runnable.args.cwd || runnable.args.workspaceRoot || ".", cwd: runnable.args.cwd || runnableArgs.workspaceRoot || ".",
sourceMap: sourceFileMap, sourceMap: sourceFileMap,
sourceLanguages: ["rust"], sourceLanguages: ["rust"],
env, env,
@ -226,6 +235,7 @@ function getCodeLldbDebugConfig(
function getNativeDebugConfig( function getNativeDebugConfig(
runnable: ra.Runnable, runnable: ra.Runnable,
runnableArgs: ra.CargoRunnableArgs,
executable: string, executable: string,
env: Record<string, string>, env: Record<string, string>,
_sourceFileMap?: Record<string, string>, _sourceFileMap?: Record<string, string>,
@ -236,8 +246,8 @@ function getNativeDebugConfig(
name: runnable.label, name: runnable.label,
target: executable, target: executable,
// See https://github.com/WebFreak001/code-debug/issues/359 // See https://github.com/WebFreak001/code-debug/issues/359
arguments: quote(runnable.args.executableArgs), arguments: quote(runnableArgs.executableArgs),
cwd: runnable.args.cwd || runnable.args.workspaceRoot || ".", cwd: runnable.args.cwd || runnableArgs.workspaceRoot || ".",
env, env,
valuesFormatting: "prettyPrinters", valuesFormatting: "prettyPrinters",
}; };

View file

@ -223,17 +223,27 @@ export type OpenCargoTomlParams = {
export type Runnable = { export type Runnable = {
label: string; label: string;
location?: lc.LocationLink; location?: lc.LocationLink;
kind: "cargo"; kind: "cargo" | "shell";
args: { args: CargoRunnableArgs | ShellRunnableArgs;
workspaceRoot?: string;
cwd?: string;
cargoArgs: string[];
cargoExtraArgs: string[];
executableArgs: string[];
expectTest?: boolean;
overrideCargo?: string;
};
}; };
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 = { export type RunnablesParams = {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;
position: lc.Position | null; position: lc.Position | null;

View file

@ -9,6 +9,7 @@ import type { Config, RunnableEnvCfg, RunnableEnvCfgItem } from "./config";
import { unwrapUndefinable } from "./undefinable"; import { unwrapUndefinable } from "./undefinable";
import type { LanguageClient } from "vscode-languageclient/node"; import type { LanguageClient } from "vscode-languageclient/node";
import type { RustEditor } from "./util"; import type { RustEditor } from "./util";
import * as toolchain from "./toolchain";
const quickPickButtons = [ const quickPickButtons = [
{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." }, { 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( export function prepareEnv(
runnable: ra.Runnable, label: string,
runnableArgs: ra.CargoRunnableArgs,
runnableEnvCfg: RunnableEnvCfg, runnableEnvCfg: RunnableEnvCfg,
): Record<string, string> { ): 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"; env["UPDATE_EXPECT"] = "1";
} }
Object.assign(env, process.env as { [key: string]: string });
const platform = process.platform; const platform = process.platform;
const checkPlatform = (it: RunnableEnvCfgItem) => { const checkPlatform = (it: RunnableEnvCfgItem) => {
@ -90,7 +97,7 @@ export function prepareEnv(
if (runnableEnvCfg) { if (runnableEnvCfg) {
if (Array.isArray(runnableEnvCfg)) { if (Array.isArray(runnableEnvCfg)) {
for (const it of 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)) { if (masked && checkPlatform(it)) {
Object.assign(env, it.env); Object.assign(env, it.env);
} }
@ -104,24 +111,41 @@ export function prepareEnv(
} }
export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> { export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> {
if (runnable.kind !== "cargo") { let definition: tasks.RustTargetDefinition;
// rust-analyzer supports only one kind, "cargo" if (runnable.kind === "cargo") {
// do not use tasks.TASK_TYPE here, these are completely different meanings. 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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate() const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate()
const task = await tasks.buildRustTask( const task = await tasks.buildRustTask(
@ -141,13 +165,13 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
return task; return task;
} }
export function createArgs(runnable: ra.Runnable): string[] { export function createCargoArgs(runnableArgs: ra.CargoRunnableArgs): string[] {
const args = [...runnable.args.cargoArgs]; // should be a copy! const args = [...runnableArgs.cargoArgs]; // should be a copy!
if (runnable.args.cargoExtraArgs) { if (runnableArgs.cargoExtraArgs) {
args.push(...runnable.args.cargoExtraArgs); // Append user-specified cargo options. args.push(...runnableArgs.cargoExtraArgs); // Append user-specified cargo options.
} }
if (runnable.args.executableArgs.length > 0) { if (runnableArgs.executableArgs.length > 0) {
args.push("--", ...runnable.args.executableArgs); args.push("--", ...runnableArgs.executableArgs);
} }
return args; return args;
} }

View file

@ -10,7 +10,7 @@ export const TASK_TYPE = "cargo";
export const TASK_SOURCE = "rust"; 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". // The cargo command, such as "run" or "check".
command: string; command: string;
// Additional arguments passed to the cargo command. // 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 // we need to inform VSCode how to execute that command by creating
// a ShellExecution for it. // a ShellExecution for it.
const definition = task.definition as CargoTaskDefinition; const definition = task.definition as RustTargetDefinition;
if (definition.type === TASK_TYPE) { if (definition.type === TASK_TYPE) {
return await buildRustTask( return await buildRustTask(
@ -87,7 +87,7 @@ class RustTaskProvider implements vscode.TaskProvider {
export async function buildRustTask( export async function buildRustTask(
scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined, scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
definition: CargoTaskDefinition, definition: RustTargetDefinition,
name: string, name: string,
problemMatcher: string[], problemMatcher: string[],
customRunner?: string, customRunner?: string,
@ -108,7 +108,7 @@ export async function buildRustTask(
} }
async function cargoToExecution( async function cargoToExecution(
definition: CargoTaskDefinition, definition: RustTargetDefinition,
customRunner: string | undefined, customRunner: string | undefined,
throwOnError: boolean, throwOnError: boolean,
): Promise<vscode.ProcessExecution | vscode.ShellExecution> { ): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
@ -138,20 +138,31 @@ async function cargoToExecution(
} }
} }
// Check whether we must use a user-defined substitute for cargo. // this is a cargo task; do Cargo-esque processing
// Split on spaces to allow overrides like "wrapper cargo". if (definition.type === TASK_TYPE) {
const cargoPath = await toolchain.cargoPath(); // Check whether we must use a user-defined substitute for cargo.
const cargoCommand = definition.overrideCargo?.split(" ") ?? [cargoPath]; // 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 args = [definition.command].concat(definition.args ?? []);
const fullCommand = [...cargoCommand, ...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), { return new vscode.ProcessExecution(definition.command, args, {
cwd: definition.cwd, cwd: definition.cwd,
env: definition.env, env: definition.env,
}); });
}
} }
export function activateTaskProvider(config: Config): vscode.Disposable { 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 { strict as nativeAssert } from "assert";
import { exec, type ExecOptions, spawnSync } from "child_process"; import { exec, type ExecOptions, spawnSync } from "child_process";
import { inspect } from "util"; import { inspect } from "util";
import type { CargoRunnableArgs, ShellRunnableArgs } from "./lsp_ext";
import type { Env } from "./client"; import type { Env } from "./client";
export function assert(condition: boolean, explanation: string): asserts condition { 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"); 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 { export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
return isRustDocument(editor.document); return isRustDocument(editor.document);
} }

View file

@ -10,6 +10,7 @@ function makeRunnable(label: string): ra.Runnable {
kind: "cargo", kind: "cargo",
args: { args: {
cargoArgs: [], cargoArgs: [],
cwd: ".",
executableArgs: [], executableArgs: [],
cargoExtraArgs: [], cargoExtraArgs: [],
}, },
@ -18,7 +19,8 @@ function makeRunnable(label: string): ra.Runnable {
function fakePrepareEnv(runnableName: string, config: RunnableEnvCfg): Record<string, string> { function fakePrepareEnv(runnableName: string, config: RunnableEnvCfg): Record<string, string> {
const runnable = makeRunnable(runnableName); 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) { export async function getTests(ctx: Context) {