mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-13 13:48:50 +00:00
feature: add build system info; runnables to rust-project.json
This commit is contained in:
parent
14a1f4530c
commit
71a78a9cdc
17 changed files with 628 additions and 227 deletions
|
@ -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;
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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 }`
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { Config, RunnableEnvCfg, RunnableEnvCfgItem } from "./config";
|
|||
import { unwrapUndefinable } from "./undefinable";
|
||||
import type { LanguageClient } from "vscode-languageclient/node";
|
||||
import type { RustEditor } from "./util";
|
||||
import * as toolchain from "./toolchain";
|
||||
|
||||
const quickPickButtons = [
|
||||
{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configuration." },
|
||||
|
@ -66,17 +67,23 @@ export class RunnableQuickPick implements vscode.QuickPickItem {
|
|||
}
|
||||
}
|
||||
|
||||
export function prepareBaseEnv(): Record<string, string> {
|
||||
const env: Record<string, string> = { RUST_BACKTRACE: "short" };
|
||||
Object.assign(env, process.env as { [key: string]: string });
|
||||
return env;
|
||||
}
|
||||
|
||||
export function prepareEnv(
|
||||
runnable: ra.Runnable,
|
||||
label: string,
|
||||
runnableArgs: ra.CargoRunnableArgs,
|
||||
runnableEnvCfg: RunnableEnvCfg,
|
||||
): Record<string, string> {
|
||||
const env: Record<string, string> = { RUST_BACKTRACE: "short" };
|
||||
const env = prepareBaseEnv();
|
||||
|
||||
if (runnable.args.expectTest) {
|
||||
if (runnableArgs.expectTest) {
|
||||
env["UPDATE_EXPECT"] = "1";
|
||||
}
|
||||
|
||||
Object.assign(env, process.env as { [key: string]: string });
|
||||
const platform = process.platform;
|
||||
|
||||
const checkPlatform = (it: RunnableEnvCfgItem) => {
|
||||
|
@ -90,7 +97,7 @@ export function prepareEnv(
|
|||
if (runnableEnvCfg) {
|
||||
if (Array.isArray(runnableEnvCfg)) {
|
||||
for (const it of runnableEnvCfg) {
|
||||
const masked = !it.mask || new RegExp(it.mask).test(runnable.label);
|
||||
const masked = !it.mask || new RegExp(it.mask).test(label);
|
||||
if (masked && checkPlatform(it)) {
|
||||
Object.assign(env, it.env);
|
||||
}
|
||||
|
@ -104,24 +111,41 @@ export function prepareEnv(
|
|||
}
|
||||
|
||||
export async function createTask(runnable: ra.Runnable, config: Config): Promise<vscode.Task> {
|
||||
if (runnable.kind !== "cargo") {
|
||||
// rust-analyzer supports only one kind, "cargo"
|
||||
// do not use tasks.TASK_TYPE here, these are completely different meanings.
|
||||
let definition: tasks.RustTargetDefinition;
|
||||
if (runnable.kind === "cargo") {
|
||||
const runnableArgs = runnable.args as ra.CargoRunnableArgs;
|
||||
let args = createCargoArgs(runnableArgs);
|
||||
|
||||
throw `Unexpected runnable kind: ${runnable.kind}`;
|
||||
let program: string;
|
||||
if (runnableArgs.overrideCargo) {
|
||||
// Split on spaces to allow overrides like "wrapper cargo".
|
||||
const cargoParts = runnableArgs.overrideCargo.split(" ");
|
||||
|
||||
program = unwrapUndefinable(cargoParts[0]);
|
||||
args = [...cargoParts.slice(1), ...args];
|
||||
} else {
|
||||
program = await toolchain.cargoPath();
|
||||
}
|
||||
|
||||
definition = {
|
||||
type: tasks.TASK_TYPE,
|
||||
command: program,
|
||||
args,
|
||||
cwd: runnableArgs.workspaceRoot || ".",
|
||||
env: prepareEnv(runnable.label, runnableArgs, config.runnablesExtraEnv),
|
||||
};
|
||||
} else {
|
||||
const runnableArgs = runnable.args as ra.ShellRunnableArgs;
|
||||
|
||||
definition = {
|
||||
type: "shell",
|
||||
command: runnableArgs.program,
|
||||
args: runnableArgs.args,
|
||||
cwd: runnableArgs.cwd,
|
||||
env: prepareBaseEnv(),
|
||||
};
|
||||
}
|
||||
|
||||
const args = createArgs(runnable);
|
||||
|
||||
const definition: tasks.CargoTaskDefinition = {
|
||||
type: tasks.TASK_TYPE,
|
||||
command: unwrapUndefinable(args[0]), // run, test, etc...
|
||||
args: args.slice(1),
|
||||
cwd: runnable.args.workspaceRoot || ".",
|
||||
env: prepareEnv(runnable, config.runnablesExtraEnv),
|
||||
overrideCargo: runnable.args.overrideCargo,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const target = vscode.workspace.workspaceFolders![0]; // safe, see main activate()
|
||||
const task = await tasks.buildRustTask(
|
||||
|
@ -141,13 +165,13 @@ export async function createTask(runnable: ra.Runnable, config: Config): Promise
|
|||
return task;
|
||||
}
|
||||
|
||||
export function createArgs(runnable: ra.Runnable): string[] {
|
||||
const args = [...runnable.args.cargoArgs]; // should be a copy!
|
||||
if (runnable.args.cargoExtraArgs) {
|
||||
args.push(...runnable.args.cargoExtraArgs); // Append user-specified cargo options.
|
||||
export function createCargoArgs(runnableArgs: ra.CargoRunnableArgs): string[] {
|
||||
const args = [...runnableArgs.cargoArgs]; // should be a copy!
|
||||
if (runnableArgs.cargoExtraArgs) {
|
||||
args.push(...runnableArgs.cargoExtraArgs); // Append user-specified cargo options.
|
||||
}
|
||||
if (runnable.args.executableArgs.length > 0) {
|
||||
args.push("--", ...runnable.args.executableArgs);
|
||||
if (runnableArgs.executableArgs.length > 0) {
|
||||
args.push("--", ...runnableArgs.executableArgs);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export const TASK_TYPE = "cargo";
|
|||
|
||||
export const TASK_SOURCE = "rust";
|
||||
|
||||
export interface CargoTaskDefinition extends vscode.TaskDefinition {
|
||||
export interface RustTargetDefinition extends vscode.TaskDefinition {
|
||||
// The cargo command, such as "run" or "check".
|
||||
command: string;
|
||||
// Additional arguments passed to the cargo command.
|
||||
|
@ -69,7 +69,7 @@ class RustTaskProvider implements vscode.TaskProvider {
|
|||
// we need to inform VSCode how to execute that command by creating
|
||||
// a ShellExecution for it.
|
||||
|
||||
const definition = task.definition as CargoTaskDefinition;
|
||||
const definition = task.definition as RustTargetDefinition;
|
||||
|
||||
if (definition.type === TASK_TYPE) {
|
||||
return await buildRustTask(
|
||||
|
@ -87,7 +87,7 @@ class RustTaskProvider implements vscode.TaskProvider {
|
|||
|
||||
export async function buildRustTask(
|
||||
scope: vscode.WorkspaceFolder | vscode.TaskScope | undefined,
|
||||
definition: CargoTaskDefinition,
|
||||
definition: RustTargetDefinition,
|
||||
name: string,
|
||||
problemMatcher: string[],
|
||||
customRunner?: string,
|
||||
|
@ -108,7 +108,7 @@ export async function buildRustTask(
|
|||
}
|
||||
|
||||
async function cargoToExecution(
|
||||
definition: CargoTaskDefinition,
|
||||
definition: RustTargetDefinition,
|
||||
customRunner: string | undefined,
|
||||
throwOnError: boolean,
|
||||
): Promise<vscode.ProcessExecution | vscode.ShellExecution> {
|
||||
|
@ -138,20 +138,31 @@ async function cargoToExecution(
|
|||
}
|
||||
}
|
||||
|
||||
// Check whether we must use a user-defined substitute for cargo.
|
||||
// Split on spaces to allow overrides like "wrapper cargo".
|
||||
const cargoPath = await toolchain.cargoPath();
|
||||
const cargoCommand = definition.overrideCargo?.split(" ") ?? [cargoPath];
|
||||
// this is a cargo task; do Cargo-esque processing
|
||||
if (definition.type === TASK_TYPE) {
|
||||
// Check whether we must use a user-defined substitute for cargo.
|
||||
// Split on spaces to allow overrides like "wrapper cargo".
|
||||
const cargoPath = await toolchain.cargoPath();
|
||||
const cargoCommand = definition.overrideCargo?.split(" ") ?? [cargoPath];
|
||||
|
||||
const args = [definition.command].concat(definition.args ?? []);
|
||||
const fullCommand = [...cargoCommand, ...args];
|
||||
const args = [definition.command].concat(definition.args ?? []);
|
||||
const fullCommand = [...cargoCommand, ...args];
|
||||
const processName = unwrapUndefinable(fullCommand[0]);
|
||||
|
||||
const processName = unwrapUndefinable(fullCommand[0]);
|
||||
return new vscode.ProcessExecution(processName, fullCommand.slice(1), {
|
||||
cwd: definition.cwd,
|
||||
env: definition.env,
|
||||
});
|
||||
} else {
|
||||
// we've been handed a process definition by rust-analyzer, trust all its inputs
|
||||
// and make a shell execution.
|
||||
const args = unwrapUndefinable(definition.args);
|
||||
|
||||
return new vscode.ProcessExecution(processName, fullCommand.slice(1), {
|
||||
cwd: definition.cwd,
|
||||
env: definition.env,
|
||||
});
|
||||
return new vscode.ProcessExecution(definition.command, args, {
|
||||
cwd: definition.cwd,
|
||||
env: definition.env,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function activateTaskProvider(config: Config): vscode.Disposable {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue