From 71a78a9cdce89b05a5d497b43fb80d4412761556 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Tue, 11 Jun 2024 11:55:04 -0400 Subject: [PATCH] feature: add build system info; runnables to `rust-project.json` --- crates/project-model/src/lib.rs | 2 +- crates/project-model/src/project_json.rs | 177 ++++++++++++++++- crates/project-model/src/workspace.rs | 2 +- crates/rust-analyzer/src/global_state.rs | 61 ++++-- crates/rust-analyzer/src/handlers/request.rs | 70 +++---- crates/rust-analyzer/src/lib.rs | 2 +- crates/rust-analyzer/src/lsp/ext.rs | 27 ++- crates/rust-analyzer/src/lsp/to_proto.rs | 184 ++++++++++++------ .../{cargo_target_spec.rs => target_spec.rs} | 110 ++++++++--- docs/dev/lsp-extensions.md | 15 +- editors/code/src/commands.ts | 7 +- editors/code/src/debug.ts | 40 ++-- editors/code/src/lsp_ext.ts | 30 ++- editors/code/src/run.ts | 76 +++++--- editors/code/src/tasks.ts | 47 +++-- editors/code/src/util.ts | 7 + editors/code/tests/unit/runnable_env.test.ts | 4 +- 17 files changed, 631 insertions(+), 230 deletions(-) rename crates/rust-analyzer/src/{cargo_target_spec.rs => target_spec.rs} (77%) diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index 181c07f46b27..35643dcc0284 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -22,7 +22,7 @@ mod cargo_workspace; mod cfg; mod env; mod manifest_path; -mod project_json; +pub mod project_json; mod rustc_cfg; mod sysroot; pub mod target_data_layout; diff --git a/crates/project-model/src/project_json.rs b/crates/project-model/src/project_json.rs index 408593ea8a68..4a916e570be2 100644 --- a/crates/project-model/src/project_json.rs +++ b/crates/project-model/src/project_json.rs @@ -33,7 +33,7 @@ //! //! * file on disk //! * a field in the config (ie, you can send a JSON request with the contents -//! of rust-project.json to rust-analyzer, no need to write anything to disk) +//! of `rust-project.json` to rust-analyzer, no need to write anything to disk) //! //! Another possible thing we don't do today, but which would be totally valid, //! is to add an extension point to VS Code extension to register custom @@ -55,8 +55,7 @@ use rustc_hash::FxHashMap; use serde::{de, Deserialize, Serialize}; use span::Edition; -use crate::cfg::CfgFlag; -use crate::ManifestPath; +use crate::{cfg::CfgFlag, ManifestPath, TargetKind}; /// Roots and crates that compose this Rust project. #[derive(Clone, Debug, Eq, PartialEq)] @@ -68,6 +67,10 @@ pub struct ProjectJson { project_root: AbsPathBuf, manifest: Option, crates: Vec, + /// Configuration for CLI commands. + /// + /// Examples include a check build or a test run. + runnables: Vec, } /// 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, pub(crate) is_proc_macro: bool, pub(crate) repository: Option, + pub build: Option, +} + +/// 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, + /// 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 { + 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, sysroot_src: Option, crates: Vec, + #[serde(default)] + runnables: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -205,6 +314,8 @@ struct CrateData { is_proc_macro: bool, #[serde(default)] repository: Option, + #[serde(default)] + build: Option, } #[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, + 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 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 for Edition { fn from(data: EditionData) -> Self { match data { @@ -231,6 +384,22 @@ impl From for Edition { } } +impl From for Runnable { + fn from(data: RunnableData) -> Self { + Runnable { program: data.program, args: data.args, cwd: data.cwd, kind: data.kind.into() } + } +} + +impl From for RunnableKind { + fn from(data: RunnableKindData) -> Self { + match data { + RunnableKindData::Check => RunnableKind::Check, + RunnableKindData::Run => RunnableKind::Run, + RunnableKindData::TestOne => RunnableKind::TestOne, + } + } +} + /// Identifies a crate by position in the crates array. /// /// This will differ from `CrateId` when multiple `ProjectJson` diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 4dba11eac3f1..17e40e74de34 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -76,7 +76,7 @@ pub enum ProjectWorkspaceKind { /// Environment variables set in the `.cargo/config` file. cargo_config_extra_env: FxHashMap, }, - /// Project workspace was manually specified using a `rust-project.json` file. + /// Project workspace was specified using a `rust-project.json` file. Json(ProjectJson), // FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning. // That's not the end user experience we should strive for. diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 3d5f525aaf9b..717d8a632c3e 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -18,10 +18,7 @@ use parking_lot::{ RwLockWriteGuard, }; use proc_macro_api::ProcMacroServer; -use project_model::{ - CargoWorkspace, ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, Target, - WorkspaceBuildScripts, -}; +use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts}; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{span, Level}; use triomphe::Arc; @@ -40,6 +37,7 @@ use crate::{ mem_docs::MemDocs, op_queue::OpQueue, reload, + target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec}, task_pool::{TaskPool, TaskQueue}, }; @@ -556,21 +554,52 @@ impl GlobalStateSnapshot { self.vfs_read().file_path(file_id).clone() } - pub(crate) fn cargo_target_for_crate_root( - &self, - crate_id: CrateId, - ) -> Option<(&CargoWorkspace, Target)> { + pub(crate) fn target_spec_for_crate(&self, crate_id: CrateId) -> Option { let file_id = self.analysis.crate_root(crate_id).ok()?; let path = self.vfs_read().file_path(file_id).clone(); let path = path.as_path()?; - self.workspaces.iter().find_map(|ws| match &ws.kind { - ProjectWorkspaceKind::Cargo { cargo, .. } - | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => { - cargo.target_by_root(path).map(|it| (cargo, it)) - } - ProjectWorkspaceKind::Json { .. } => None, - ProjectWorkspaceKind::DetachedFile { .. } => None, - }) + + for workspace in self.workspaces.iter() { + match &workspace.kind { + ProjectWorkspaceKind::Cargo { cargo, .. } + | ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => { + let Some(target_idx) = cargo.target_by_root(path) else { + continue; + }; + + let target_data = &cargo[target_idx]; + let package_data = &cargo[target_data.package]; + + return Some(TargetSpec::Cargo(CargoTargetSpec { + workspace_root: cargo.workspace_root().to_path_buf(), + cargo_toml: package_data.manifest.clone(), + crate_id, + package: cargo.package_flag(package_data), + target: target_data.name.clone(), + target_kind: target_data.kind, + required_features: target_data.required_features.clone(), + features: package_data.features.keys().cloned().collect(), + })); + } + ProjectWorkspaceKind::Json(project) => { + let Some(krate) = project.crate_by_root(path) else { + continue; + }; + let Some(build) = krate.build else { + continue; + }; + + return Some(TargetSpec::ProjectJson(ProjectJsonTargetSpec { + label: build.label, + target_kind: build.target_kind, + shell_runnables: project.runnables().to_owned(), + })); + } + ProjectWorkspaceKind::DetachedFile { .. } => {} + }; + } + + None } pub(crate) fn file_exists(&self, file_id: FileId) -> bool { diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 0789dd646239..8e39b15da3dd 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -35,7 +35,6 @@ use triomphe::Arc; use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use crate::{ - cargo_target_spec::CargoTargetSpec, config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, diff::diff, global_state::{GlobalState, GlobalStateSnapshot}, @@ -51,6 +50,7 @@ use crate::{ self, CrateInfoResult, ExternalDocsPair, ExternalDocsResponse, FetchDependencyListParams, FetchDependencyListResult, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams, }, + target_spec::TargetSpec, }; pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> anyhow::Result<()> { @@ -790,9 +790,9 @@ pub(crate) fn handle_parent_module( Some(&crate_id) => crate_id, None => return Ok(None), }; - let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? { - Some(it) => it, - None => return Ok(None), + let cargo_spec = match TargetSpec::for_file(&snap, file_id)? { + Some(TargetSpec::Cargo(it)) => it, + Some(TargetSpec::ProjectJson(_)) | None => return Ok(None), }; if snap.analysis.crate_root(crate_id)? == file_id { @@ -823,7 +823,7 @@ pub(crate) fn handle_runnables( let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; let line_index = snap.file_line_index(file_id)?; let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok()); - let cargo_spec = CargoTargetSpec::for_file(&snap, file_id)?; + let target_spec = TargetSpec::for_file(&snap, file_id)?; let expect_test = match offset { Some(offset) => { @@ -840,21 +840,24 @@ pub(crate) fn handle_runnables( if should_skip_for_offset(&runnable, offset) { continue; } - if should_skip_target(&runnable, cargo_spec.as_ref()) { + if should_skip_target(&runnable, target_spec.as_ref()) { continue; } - let mut runnable = to_proto::runnable(&snap, runnable)?; - if expect_test { - runnable.label = format!("{} + expect", runnable.label); - runnable.args.expect_test = Some(true); + if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? { + if expect_test { + if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args { + runnable.label = format!("{} + expect", runnable.label); + r.expect_test = Some(true); + } + } + res.push(runnable); } - res.push(runnable); } // Add `cargo check` and `cargo test` for all targets of the whole package let config = snap.config.runnables(); - match cargo_spec { - Some(spec) => { + match target_spec { + Some(TargetSpec::Cargo(spec)) => { let is_crate_no_std = snap.analysis.is_crate_no_std(spec.crate_id)?; for cmd in ["check", "run", "test"] { if cmd == "run" && spec.target_kind != TargetKind::Bin { @@ -879,7 +882,7 @@ pub(crate) fn handle_runnables( ), location: None, kind: lsp_ext::RunnableKind::Cargo, - args: lsp_ext::CargoRunnable { + args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs { workspace_root: Some(spec.workspace_root.clone().into()), cwd: Some(cwd.into()), override_cargo: config.override_cargo.clone(), @@ -887,17 +890,18 @@ pub(crate) fn handle_runnables( cargo_extra_args: config.cargo_extra_args.clone(), executable_args: Vec::new(), expect_test: None, - }, + }), }) } } + Some(TargetSpec::ProjectJson(_)) => {} None => { if !snap.config.linked_or_discovered_projects().is_empty() { res.push(lsp_ext::Runnable { label: "cargo check --workspace".to_owned(), location: None, kind: lsp_ext::RunnableKind::Cargo, - args: lsp_ext::CargoRunnable { + args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs { workspace_root: None, cwd: None, override_cargo: config.override_cargo, @@ -905,7 +909,7 @@ pub(crate) fn handle_runnables( cargo_extra_args: config.cargo_extra_args, executable_args: Vec::new(), expect_test: None, - }, + }), }); } } @@ -931,7 +935,7 @@ pub(crate) fn handle_related_tests( let tests = snap.analysis.related_tests(position, None)?; let mut res = Vec::new(); for it in tests { - if let Ok(runnable) = to_proto::runnable(&snap, it) { + if let Ok(Some(runnable)) = to_proto::runnable(&snap, it) { res.push(lsp_ext::TestInfo { runnable }) } } @@ -1397,14 +1401,14 @@ pub(crate) fn handle_code_lens( } let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; - let cargo_target_spec = CargoTargetSpec::for_file(&snap, file_id)?; + let target_spec = TargetSpec::for_file(&snap, file_id)?; let annotations = snap.analysis.annotations( &AnnotationConfig { - binary_target: cargo_target_spec + binary_target: target_spec .map(|spec| { matches!( - spec.target_kind, + spec.target_kind(), TargetKind::Bin | TargetKind::Example | TargetKind::Test ) }) @@ -1824,9 +1828,9 @@ pub(crate) fn handle_open_cargo_toml( let _p = tracing::info_span!("handle_open_cargo_toml").entered(); let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; - let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? { - Some(it) => it, - None => return Ok(None), + let cargo_spec = match TargetSpec::for_file(&snap, file_id)? { + Some(TargetSpec::Cargo(it)) => it, + Some(TargetSpec::ProjectJson(_)) | None => return Ok(None), }; let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml); @@ -1954,8 +1958,8 @@ fn runnable_action_links( return None; } - let cargo_spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id).ok()?; - if should_skip_target(&runnable, cargo_spec.as_ref()) { + let target_spec = TargetSpec::for_file(snap, runnable.nav.file_id).ok()?; + if should_skip_target(&runnable, target_spec.as_ref()) { return None; } @@ -1965,7 +1969,7 @@ fn runnable_action_links( } let title = runnable.title(); - let r = to_proto::runnable(snap, runnable).ok()?; + let r = to_proto::runnable(snap, runnable).ok()??; let mut group = lsp_ext::CommandLinkGroup::default(); @@ -2020,13 +2024,13 @@ fn prepare_hover_actions( .collect() } -fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>) -> bool { +fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&TargetSpec>) -> bool { match runnable.kind { RunnableKind::Bin => { // Do not suggest binary run on other target than binary match &cargo_spec { Some(spec) => !matches!( - spec.target_kind, + spec.target_kind(), TargetKind::Bin | TargetKind::Example | TargetKind::Test ), None => true, @@ -2103,9 +2107,9 @@ fn run_rustfmt( } RustfmtConfig::CustomCommand { command, args } => { let cmd = Utf8PathBuf::from(&command); - let workspace = CargoTargetSpec::for_file(snap, file_id)?; - let mut cmd = match workspace { - Some(spec) => { + let target_spec = TargetSpec::for_file(snap, file_id)?; + let mut cmd = match target_spec { + Some(TargetSpec::Cargo(spec)) => { // approach: if the command name contains a path separator, join it with the workspace root. // however, if the path is absolute, joining will result in the absolute path being preserved. // as a fallback, rely on $PATH-based discovery. @@ -2118,7 +2122,7 @@ fn run_rustfmt( }; process::Command::new(cmd_path) } - None => process::Command::new(cmd), + _ => process::Command::new(cmd), }; cmd.envs(snap.config.extra_env()); diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index b3c11d0156ed..a398e98f093b 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -14,7 +14,6 @@ pub mod cli; mod caps; -mod cargo_target_spec; mod diagnostics; mod diff; mod dispatch; @@ -24,6 +23,7 @@ mod main_loop; mod mem_docs; mod op_queue; mod reload; +mod target_spec; mod task_pool; mod version; diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs index 4da9054d13de..b82ba4419041 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -3,7 +3,6 @@ #![allow(clippy::disallowed_types)] use std::ops; -use std::path::PathBuf; use ide_db::line_index::WideEncoding; use lsp_types::request::Request; @@ -12,6 +11,7 @@ use lsp_types::{ PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams, }; use lsp_types::{PositionEncodingKind, Url}; +use paths::Utf8PathBuf; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -439,24 +439,33 @@ pub struct Runnable { #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, 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, #[serde(skip_serializing_if = "Option::is_none")] - pub workspace_root: Option, + pub workspace_root: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub cwd: Option, + pub cwd: Option, // command, --package and --lib stuff pub cargo_args: Vec, // user-specified additional cargo args, like `--release`. @@ -467,6 +476,14 @@ pub struct CargoRunnable { pub expect_test: Option, } +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ShellRunnableArgs { + pub program: String, + pub args: Vec, + pub cwd: Utf8PathBuf, +} + pub enum RelatedTests {} impl Request for RelatedTests { diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index 86368c9eea87..db5f666a5b95 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -21,16 +21,17 @@ use serde_json::to_value; use vfs::AbsPath; use crate::{ - cargo_target_spec::CargoTargetSpec, config::{CallInfoConfig, Config}, global_state::GlobalStateSnapshot, line_index::{LineEndings, LineIndex, PositionEncoding}, lsp::{ + ext::ShellRunnableArgs, semantic_tokens::{self, standard_fallback_type}, utils::invalid_params_error, LspError, }, lsp_ext::{self, SnippetTextEdit}, + target_spec::{CargoTargetSpec, TargetSpec}, }; pub(crate) fn position(line_index: &LineIndex, offset: TextSize) -> lsp_types::Position { @@ -1356,34 +1357,90 @@ pub(crate) fn code_action( pub(crate) fn runnable( snap: &GlobalStateSnapshot, runnable: Runnable, -) -> Cancellable { +) -> Cancellable> { let config = snap.config.runnables(); - let spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id)?; - let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone()); - let cwd = match runnable.kind { - ide::RunnableKind::Bin { .. } => workspace_root.clone().map(|it| it.into()), - _ => spec.as_ref().map(|it| it.cargo_toml.parent().into()), - }; - let target = spec.as_ref().map(|s| s.target.as_str()); - let label = runnable.label(target); - let (cargo_args, executable_args) = - CargoTargetSpec::runnable_args(snap, spec, &runnable.kind, &runnable.cfg); - let location = location_link(snap, None, runnable.nav)?; + let target_spec = TargetSpec::for_file(snap, runnable.nav.file_id)?; - Ok(lsp_ext::Runnable { - label, - location: Some(location), - kind: lsp_ext::RunnableKind::Cargo, - args: lsp_ext::CargoRunnable { - workspace_root: workspace_root.map(|it| it.into()), - cwd, - override_cargo: config.override_cargo, - cargo_args, - cargo_extra_args: config.cargo_extra_args, - executable_args, - expect_test: None, - }, - }) + match target_spec { + Some(TargetSpec::Cargo(spec)) => { + let workspace_root = spec.workspace_root.clone(); + + let target = spec.target.clone(); + + let (cargo_args, executable_args) = CargoTargetSpec::runnable_args( + snap, + Some(spec.clone()), + &runnable.kind, + &runnable.cfg, + ); + + let cwd = match runnable.kind { + ide::RunnableKind::Bin { .. } => workspace_root.clone(), + _ => spec.cargo_toml.parent().to_owned(), + }; + + let label = runnable.label(Some(&target)); + let location = location_link(snap, None, runnable.nav)?; + + Ok(Some(lsp_ext::Runnable { + label, + location: Some(location), + kind: lsp_ext::RunnableKind::Cargo, + args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs { + workspace_root: Some(workspace_root.into()), + override_cargo: config.override_cargo, + cargo_args, + cwd: Some(cwd.into()), + cargo_extra_args: config.cargo_extra_args, + executable_args, + expect_test: None, + }), + })) + } + Some(TargetSpec::ProjectJson(spec)) => { + let label = runnable.label(Some(&spec.label)); + let location = location_link(snap, None, runnable.nav)?; + + match spec.runnable_args(&runnable.kind) { + Some(json_shell_runnable_args) => { + let runnable_args = ShellRunnableArgs { + program: json_shell_runnable_args.program, + args: json_shell_runnable_args.args, + cwd: json_shell_runnable_args.cwd, + }; + Ok(Some(lsp_ext::Runnable { + label, + location: Some(location), + kind: lsp_ext::RunnableKind::Shell, + args: lsp_ext::RunnableArgs::Shell(runnable_args), + })) + } + None => Ok(None), + } + } + None => { + let (cargo_args, executable_args) = + CargoTargetSpec::runnable_args(snap, None, &runnable.kind, &runnable.cfg); + + let label = runnable.label(None); + let location = location_link(snap, None, runnable.nav)?; + + Ok(Some(lsp_ext::Runnable { + label, + location: Some(location), + kind: lsp_ext::RunnableKind::Cargo, + args: lsp_ext::RunnableArgs::Cargo(lsp_ext::CargoRunnableArgs { + workspace_root: None, + override_cargo: config.override_cargo, + cargo_args, + cwd: None, + cargo_extra_args: config.cargo_extra_args, + executable_args, + expect_test: None, + }), + })) + } + } } pub(crate) fn code_lens( @@ -1407,33 +1464,37 @@ pub(crate) fn code_lens( }; let r = runnable(snap, run)?; - let lens_config = snap.config.lens(); - if lens_config.run - && client_commands_config.run_single - && r.args.workspace_root.is_some() - { - let command = command::run_single(&r, &title); - acc.push(lsp_types::CodeLens { - range: annotation_range, - command: Some(command), - data: None, - }) - } - if lens_config.debug && can_debug && client_commands_config.debug_single { - let command = command::debug_single(&r); - acc.push(lsp_types::CodeLens { - range: annotation_range, - command: Some(command), - data: None, - }) - } - if lens_config.interpret { - let command = command::interpret_single(&r); - acc.push(lsp_types::CodeLens { - range: annotation_range, - command: Some(command), - data: None, - }) + if let Some(r) = r { + let has_root = match &r.args { + lsp_ext::RunnableArgs::Cargo(c) => c.workspace_root.is_some(), + lsp_ext::RunnableArgs::Shell(_) => true, + }; + + let lens_config = snap.config.lens(); + if lens_config.run && client_commands_config.run_single && has_root { + let command = command::run_single(&r, &title); + acc.push(lsp_types::CodeLens { + range: annotation_range, + command: Some(command), + data: None, + }) + } + if lens_config.debug && can_debug && client_commands_config.debug_single { + let command = command::debug_single(&r); + acc.push(lsp_types::CodeLens { + range: annotation_range, + command: Some(command), + data: None, + }) + } + if lens_config.interpret { + let command = command::interpret_single(&r); + acc.push(lsp_types::CodeLens { + range: annotation_range, + command: Some(command), + data: None, + }) + } } } AnnotationKind::HasImpls { pos, data } => { @@ -1538,12 +1599,8 @@ pub(crate) fn test_item( id: test_item.id, label: test_item.label, kind: match test_item.kind { - ide::TestItemKind::Crate(id) => 'b: { - let Some((cargo_ws, target)) = snap.cargo_target_for_crate_root(id) else { - break 'b lsp_ext::TestItemKind::Package; - }; - let target = &cargo_ws[target]; - match target.kind { + ide::TestItemKind::Crate(id) => match snap.target_spec_for_crate(id) { + Some(target_spec) => match target_spec.target_kind() { project_model::TargetKind::Bin | project_model::TargetKind::Lib { .. } | project_model::TargetKind::Example @@ -1552,8 +1609,9 @@ pub(crate) fn test_item( project_model::TargetKind::Test => lsp_ext::TestItemKind::Test, // benches are not tests needed to be shown in the test explorer project_model::TargetKind::Bench => return None, - } - } + }, + None => lsp_ext::TestItemKind::Package, + }, ide::TestItemKind::Module => lsp_ext::TestItemKind::Module, ide::TestItemKind::Function => lsp_ext::TestItemKind::Test, }, @@ -1566,7 +1624,7 @@ pub(crate) fn test_item( .file .map(|f| lsp_types::TextDocumentIdentifier { uri: url(snap, f) }), range: line_index.and_then(|l| Some(range(l, test_item.text_range?))), - runnable: test_item.runnable.and_then(|r| runnable(snap, r).ok()), + runnable: test_item.runnable.and_then(|r| runnable(snap, r).ok()).flatten(), }) } diff --git a/crates/rust-analyzer/src/cargo_target_spec.rs b/crates/rust-analyzer/src/target_spec.rs similarity index 77% rename from crates/rust-analyzer/src/cargo_target_spec.rs rename to crates/rust-analyzer/src/target_spec.rs index 693a35b91e61..6145f7e05f9a 100644 --- a/crates/rust-analyzer/src/cargo_target_spec.rs +++ b/crates/rust-analyzer/src/target_spec.rs @@ -1,20 +1,52 @@ -//! See `CargoTargetSpec` +//! See `TargetSpec` use std::mem; use cfg::{CfgAtom, CfgExpr}; use ide::{Cancellable, CrateId, FileId, RunnableKind, TestId}; +use project_model::project_json::Runnable; use project_model::{CargoFeatures, ManifestPath, TargetKind}; use rustc_hash::FxHashSet; use vfs::AbsPathBuf; use crate::global_state::GlobalStateSnapshot; +/// A target represents a thing we can build or test. +/// +/// We use it to calculate the CLI arguments required to build, run or +/// test the target. +#[derive(Clone, Debug)] +pub(crate) enum TargetSpec { + Cargo(CargoTargetSpec), + ProjectJson(ProjectJsonTargetSpec), +} + +impl TargetSpec { + pub(crate) fn for_file( + global_state_snapshot: &GlobalStateSnapshot, + file_id: FileId, + ) -> Cancellable> { + 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, } +#[derive(Clone, Debug)] +pub(crate) struct ProjectJsonTargetSpec { + pub(crate) label: String, + pub(crate) target_kind: TargetKind, + pub(crate) shell_runnables: Vec, +} + +impl ProjectJsonTargetSpec { + pub(crate) fn runnable_args(&self, kind: &RunnableKind) -> Option { + 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> { - 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, kind: &RunnableKind) { buf.push("--package".to_owned()); buf.push(self.package); diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 100662f4cebd..695fec7e8e01 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@