Skip to content

Commit a97aef8

Browse files
committed
Auto merge of #16840 - Wilfred:shell_runnable, r=Veykril
Allow rust-project.json to include arbitrary shell commands for runnables This is a follow-up on #16135, resolving the feedback raised :) Allow rust-project.json to include shell runnables, of the form: ``` { "build_info": { "label": "//project/foo:my-crate", "target_kind": "bin", "shell_runnables": [ { "kind": "run", "program": "buck2", "args": ["run", "//project/foo:my-crate"] }, { "kind": "test_one", "program": "test_runner", "args": ["--name=$$TEST_NAME$$"] } ] } } ``` If these runnable configs are present for the current crate in rust-project.json, offer them as runnables in VS Code. This PR required some boring changes to APIs that previously only handled cargo situations. I've split out these changes as commits labelled 'refactor', so it's easy to see the interesting changes.
2 parents 5e1ab70 + 1e9e86c commit a97aef8

File tree

17 files changed

+631
-230
lines changed

17 files changed

+631
-230
lines changed

src/tools/rust-analyzer/crates/project-model/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod cargo_workspace;
2222
mod cfg;
2323
mod env;
2424
mod manifest_path;
25-
mod project_json;
25+
pub mod project_json;
2626
mod rustc_cfg;
2727
mod sysroot;
2828
pub mod target_data_layout;

src/tools/rust-analyzer/crates/project-model/src/project_json.rs

+173-4
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
//!
3434
//! * file on disk
3535
//! * a field in the config (ie, you can send a JSON request with the contents
36-
//! of rust-project.json to rust-analyzer, no need to write anything to disk)
36+
//! of `rust-project.json` to rust-analyzer, no need to write anything to disk)
3737
//!
3838
//! Another possible thing we don't do today, but which would be totally valid,
3939
//! is to add an extension point to VS Code extension to register custom
@@ -55,8 +55,7 @@ use rustc_hash::FxHashMap;
5555
use serde::{de, Deserialize, Serialize};
5656
use span::Edition;
5757

58-
use crate::cfg::CfgFlag;
59-
use crate::ManifestPath;
58+
use crate::{cfg::CfgFlag, ManifestPath, TargetKind};
6059

6160
/// Roots and crates that compose this Rust project.
6261
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -68,6 +67,10 @@ pub struct ProjectJson {
6867
project_root: AbsPathBuf,
6968
manifest: Option<ManifestPath>,
7069
crates: Vec<Crate>,
70+
/// Configuration for CLI commands.
71+
///
72+
/// Examples include a check build or a test run.
73+
runnables: Vec<Runnable>,
7174
}
7275

7376
/// A crate points to the root module of a crate and lists the dependencies of the crate. This is
@@ -88,13 +91,94 @@ pub struct Crate {
8891
pub(crate) exclude: Vec<AbsPathBuf>,
8992
pub(crate) is_proc_macro: bool,
9093
pub(crate) repository: Option<String>,
94+
pub build: Option<Build>,
95+
}
96+
97+
/// Additional, build-specific data about a crate.
98+
#[derive(Clone, Debug, Eq, PartialEq)]
99+
pub struct Build {
100+
/// The name associated with this crate.
101+
///
102+
/// This is determined by the build system that produced
103+
/// the `rust-project.json` in question. For instance, if buck were used,
104+
/// the label might be something like `//ide/rust/rust-analyzer:rust-analyzer`.
105+
///
106+
/// Do not attempt to parse the contents of this string; it is a build system-specific
107+
/// identifier similar to [`Crate::display_name`].
108+
pub label: String,
109+
/// Path corresponding to the build system-specific file defining the crate.
110+
///
111+
/// It is roughly analogous to [`ManifestPath`], but it should *not* be used with
112+
/// [`crate::ProjectManifest::from_manifest_file`], as the build file may not be
113+
/// be in the `rust-project.json`.
114+
pub build_file: Utf8PathBuf,
115+
/// The kind of target.
116+
///
117+
/// Examples (non-exhaustively) include [`TargetKind::Bin`], [`TargetKind::Lib`],
118+
/// and [`TargetKind::Test`]. This information is used to determine what sort
119+
/// of runnable codelens to provide, if any.
120+
pub target_kind: TargetKind,
121+
}
122+
123+
/// A template-like structure for describing runnables.
124+
///
125+
/// These are used for running and debugging binaries and tests without encoding
126+
/// build system-specific knowledge into rust-analyzer.
127+
///
128+
/// # Example
129+
///
130+
/// Below is an example of a test runnable. `{label}` and `{test_id}`
131+
/// are explained in [`Runnable::args`]'s documentation.
132+
///
133+
/// ```json
134+
/// {
135+
/// "program": "buck",
136+
/// "args": [
137+
/// "test",
138+
/// "{label}",
139+
/// "--",
140+
/// "{test_id}",
141+
/// "--print-passing-details"
142+
/// ],
143+
/// "cwd": "/home/user/repo-root/",
144+
/// "kind": "testOne"
145+
/// }
146+
/// ```
147+
#[derive(Debug, Clone, PartialEq, Eq)]
148+
pub struct Runnable {
149+
/// The program invoked by the runnable.
150+
///
151+
/// For example, this might be `cargo`, `buck`, or `bazel`.
152+
pub program: String,
153+
/// The arguments passed to [`Runnable::program`].
154+
///
155+
/// The args can contain two template strings: `{label}` and `{test_id}`.
156+
/// rust-analyzer will find and replace `{label}` with [`Build::label`] and
157+
/// `{test_id}` with the test name.
158+
pub args: Vec<String>,
159+
/// The current working directory of the runnable.
160+
pub cwd: Utf8PathBuf,
161+
pub kind: RunnableKind,
162+
}
163+
164+
/// The kind of runnable.
165+
#[derive(Debug, Clone, PartialEq, Eq)]
166+
pub enum RunnableKind {
167+
Check,
168+
169+
/// Can run a binary.
170+
Run,
171+
172+
/// Run a single test.
173+
TestOne,
91174
}
92175

93176
impl ProjectJson {
94177
/// Create a new ProjectJson instance.
95178
///
96179
/// # Arguments
97180
///
181+
/// * `manifest` - The path to the `rust-project.json`.
98182
/// * `base` - The path to the workspace root (i.e. the folder containing `rust-project.json`)
99183
/// * `data` - The parsed contents of `rust-project.json`, or project json that's passed via
100184
/// configuration.
@@ -109,6 +193,7 @@ impl ProjectJson {
109193
sysroot_src: data.sysroot_src.map(absolutize_on_base),
110194
project_root: base.to_path_buf(),
111195
manifest,
196+
runnables: data.runnables.into_iter().map(Runnable::from).collect(),
112197
crates: data
113198
.crates
114199
.into_iter()
@@ -127,6 +212,15 @@ impl ProjectJson {
127212
None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()),
128213
};
129214

215+
let build = match crate_data.build {
216+
Some(build) => Some(Build {
217+
label: build.label,
218+
build_file: build.build_file,
219+
target_kind: build.target_kind.into(),
220+
}),
221+
None => None,
222+
};
223+
130224
Crate {
131225
display_name: crate_data
132226
.display_name
@@ -146,6 +240,7 @@ impl ProjectJson {
146240
exclude,
147241
is_proc_macro: crate_data.is_proc_macro,
148242
repository: crate_data.repository,
243+
build,
149244
}
150245
})
151246
.collect(),
@@ -167,7 +262,15 @@ impl ProjectJson {
167262
&self.project_root
168263
}
169264

170-
/// Returns the path to the project's manifest file, if it exists.
265+
pub fn crate_by_root(&self, root: &AbsPath) -> Option<Crate> {
266+
self.crates
267+
.iter()
268+
.filter(|krate| krate.is_workspace_member)
269+
.find(|krate| krate.root_module == root)
270+
.cloned()
271+
}
272+
273+
/// Returns the path to the project's manifest, if it exists.
171274
pub fn manifest(&self) -> Option<&ManifestPath> {
172275
self.manifest.as_ref()
173276
}
@@ -176,13 +279,19 @@ impl ProjectJson {
176279
pub fn manifest_or_root(&self) -> &AbsPath {
177280
self.manifest.as_ref().map_or(&self.project_root, |manifest| manifest.as_ref())
178281
}
282+
283+
pub fn runnables(&self) -> &[Runnable] {
284+
&self.runnables
285+
}
179286
}
180287

181288
#[derive(Serialize, Deserialize, Debug, Clone)]
182289
pub struct ProjectJsonData {
183290
sysroot: Option<Utf8PathBuf>,
184291
sysroot_src: Option<Utf8PathBuf>,
185292
crates: Vec<CrateData>,
293+
#[serde(default)]
294+
runnables: Vec<RunnableData>,
186295
}
187296

188297
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -205,6 +314,8 @@ struct CrateData {
205314
is_proc_macro: bool,
206315
#[serde(default)]
207316
repository: Option<String>,
317+
#[serde(default)]
318+
build: Option<BuildData>,
208319
}
209320

210321
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -220,6 +331,48 @@ enum EditionData {
220331
Edition2024,
221332
}
222333

334+
#[derive(Debug, Clone, Serialize, Deserialize)]
335+
pub struct BuildData {
336+
label: String,
337+
build_file: Utf8PathBuf,
338+
target_kind: TargetKindData,
339+
}
340+
341+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342+
pub struct RunnableData {
343+
pub program: String,
344+
pub args: Vec<String>,
345+
pub cwd: Utf8PathBuf,
346+
pub kind: RunnableKindData,
347+
}
348+
349+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
350+
#[serde(rename_all = "camelCase")]
351+
pub enum RunnableKindData {
352+
Check,
353+
Run,
354+
TestOne,
355+
}
356+
357+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
358+
#[serde(rename_all = "camelCase")]
359+
pub enum TargetKindData {
360+
Bin,
361+
/// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
362+
Lib,
363+
Test,
364+
}
365+
366+
impl From<TargetKindData> for TargetKind {
367+
fn from(data: TargetKindData) -> Self {
368+
match data {
369+
TargetKindData::Bin => TargetKind::Bin,
370+
TargetKindData::Lib => TargetKind::Lib { is_proc_macro: false },
371+
TargetKindData::Test => TargetKind::Test,
372+
}
373+
}
374+
}
375+
223376
impl From<EditionData> for Edition {
224377
fn from(data: EditionData) -> Self {
225378
match data {
@@ -231,6 +384,22 @@ impl From<EditionData> for Edition {
231384
}
232385
}
233386

387+
impl From<RunnableData> for Runnable {
388+
fn from(data: RunnableData) -> Self {
389+
Runnable { program: data.program, args: data.args, cwd: data.cwd, kind: data.kind.into() }
390+
}
391+
}
392+
393+
impl From<RunnableKindData> for RunnableKind {
394+
fn from(data: RunnableKindData) -> Self {
395+
match data {
396+
RunnableKindData::Check => RunnableKind::Check,
397+
RunnableKindData::Run => RunnableKind::Run,
398+
RunnableKindData::TestOne => RunnableKind::TestOne,
399+
}
400+
}
401+
}
402+
234403
/// Identifies a crate by position in the crates array.
235404
///
236405
/// This will differ from `CrateId` when multiple `ProjectJson`

src/tools/rust-analyzer/crates/project-model/src/workspace.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub enum ProjectWorkspaceKind {
7676
/// Environment variables set in the `.cargo/config` file.
7777
cargo_config_extra_env: FxHashMap<String, String>,
7878
},
79-
/// Project workspace was manually specified using a `rust-project.json` file.
79+
/// Project workspace was specified using a `rust-project.json` file.
8080
Json(ProjectJson),
8181
// FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning.
8282
// That's not the end user experience we should strive for.

src/tools/rust-analyzer/crates/rust-analyzer/src/global_state.rs

+45-16
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ use parking_lot::{
1818
RwLockWriteGuard,
1919
};
2020
use proc_macro_api::ProcMacroServer;
21-
use project_model::{
22-
CargoWorkspace, ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, Target,
23-
WorkspaceBuildScripts,
24-
};
21+
use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
2522
use rustc_hash::{FxHashMap, FxHashSet};
2623
use tracing::{span, Level};
2724
use triomphe::Arc;
@@ -40,6 +37,7 @@ use crate::{
4037
mem_docs::MemDocs,
4138
op_queue::OpQueue,
4239
reload,
40+
target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec},
4341
task_pool::{TaskPool, TaskQueue},
4442
};
4543

@@ -556,21 +554,52 @@ impl GlobalStateSnapshot {
556554
self.vfs_read().file_path(file_id).clone()
557555
}
558556

559-
pub(crate) fn cargo_target_for_crate_root(
560-
&self,
561-
crate_id: CrateId,
562-
) -> Option<(&CargoWorkspace, Target)> {
557+
pub(crate) fn target_spec_for_crate(&self, crate_id: CrateId) -> Option<TargetSpec> {
563558
let file_id = self.analysis.crate_root(crate_id).ok()?;
564559
let path = self.vfs_read().file_path(file_id).clone();
565560
let path = path.as_path()?;
566-
self.workspaces.iter().find_map(|ws| match &ws.kind {
567-
ProjectWorkspaceKind::Cargo { cargo, .. }
568-
| ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
569-
cargo.target_by_root(path).map(|it| (cargo, it))
570-
}
571-
ProjectWorkspaceKind::Json { .. } => None,
572-
ProjectWorkspaceKind::DetachedFile { .. } => None,
573-
})
561+
562+
for workspace in self.workspaces.iter() {
563+
match &workspace.kind {
564+
ProjectWorkspaceKind::Cargo { cargo, .. }
565+
| ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
566+
let Some(target_idx) = cargo.target_by_root(path) else {
567+
continue;
568+
};
569+
570+
let target_data = &cargo[target_idx];
571+
let package_data = &cargo[target_data.package];
572+
573+
return Some(TargetSpec::Cargo(CargoTargetSpec {
574+
workspace_root: cargo.workspace_root().to_path_buf(),
575+
cargo_toml: package_data.manifest.clone(),
576+
crate_id,
577+
package: cargo.package_flag(package_data),
578+
target: target_data.name.clone(),
579+
target_kind: target_data.kind,
580+
required_features: target_data.required_features.clone(),
581+
features: package_data.features.keys().cloned().collect(),
582+
}));
583+
}
584+
ProjectWorkspaceKind::Json(project) => {
585+
let Some(krate) = project.crate_by_root(path) else {
586+
continue;
587+
};
588+
let Some(build) = krate.build else {
589+
continue;
590+
};
591+
592+
return Some(TargetSpec::ProjectJson(ProjectJsonTargetSpec {
593+
label: build.label,
594+
target_kind: build.target_kind,
595+
shell_runnables: project.runnables().to_owned(),
596+
}));
597+
}
598+
ProjectWorkspaceKind::DetachedFile { .. } => {}
599+
};
600+
}
601+
602+
None
574603
}
575604

576605
pub(crate) fn file_exists(&self, file_id: FileId) -> bool {

0 commit comments

Comments
 (0)