diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index 28696aa32775..f8b90b55752c 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -21,7 +21,7 @@ mod build_scripts; mod cargo_workspace; mod cfg_flag; 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 512588cc8f8f..9e2389779833 100644 --- a/crates/project-model/src/project_json.rs +++ b/crates/project-model/src/project_json.rs @@ -55,8 +55,9 @@ use paths::{AbsPath, AbsPathBuf, Utf8PathBuf}; use rustc_hash::FxHashMap; use serde::{de, Deserialize}; use span::Edition; +use std::path::PathBuf; -use crate::cfg_flag::CfgFlag; +use crate::{cfg_flag::CfgFlag, TargetKind}; /// Roots and crates that compose this Rust project. #[derive(Clone, Debug, Eq, PartialEq)] @@ -87,6 +88,21 @@ pub struct Crate { pub(crate) exclude: Vec, pub(crate) is_proc_macro: bool, pub(crate) repository: Option, + pub build_info: Option, +} + +/// Additional metadata about a crate, used to configure runnables. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BuildInfo { + /// The name associated with this crate, according to the custom + /// build system being used. + pub label: String, + /// What kind of target is this crate? For example, we don't want + /// to offer a 'run' button for library crates. + pub target_kind: TargetKind, + /// Configuration for shell commands, such as CLI invocations for + /// a check build or a test run. + pub shell_runnables: Vec, } impl ProjectJson { @@ -121,6 +137,15 @@ impl ProjectJson { None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()), }; + let build_info = match crate_data.build_info { + Some(build_info) => Some(BuildInfo { + label: build_info.label, + target_kind: build_info.target_kind.into(), + shell_runnables: build_info.shell_runnables, + }), + None => None, + }; + Crate { display_name: crate_data .display_name @@ -149,6 +174,7 @@ impl ProjectJson { exclude, is_proc_macro: crate_data.is_proc_macro, repository: crate_data.repository, + build_info, } }) .collect(), @@ -172,6 +198,14 @@ impl ProjectJson { pub fn path(&self) -> &AbsPath { &self.project_root } + + 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() + } } #[derive(Deserialize, Debug, Clone)] @@ -201,6 +235,8 @@ struct CrateData { is_proc_macro: bool, #[serde(default)] repository: Option, + #[serde(default)] + build_info: Option, } #[derive(Deserialize, Debug, Clone)] @@ -216,6 +252,48 @@ enum EditionData { Edition2024, } +#[derive(Deserialize, Debug, Clone)] +pub struct BuildInfoData { + label: String, + target_kind: TargetKindData, + shell_runnables: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ShellRunnableArgs { + pub program: String, + pub args: Vec, + pub cwd: PathBuf, + pub kind: ShellRunnableKind, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ShellRunnableKind { + Check, + Run, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[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(value: TargetKindData) -> Self { + match value { + 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 { diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 123527c8e52e..ecaec9fe6cc6 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -33,7 +33,7 @@ use crate::{ mem_docs::MemDocs, op_queue::OpQueue, reload, - target_spec::{CargoTargetSpec, TargetSpec}, + target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec}, task_pool::{TaskPool, TaskQueue}, }; @@ -529,7 +529,20 @@ impl GlobalStateSnapshot { features: package_data.features.keys().cloned().collect(), })); } - ProjectWorkspace::Json { .. } => {} + ProjectWorkspace::Json { project, .. } => { + let Some(krate) = project.crate_by_root(path) else { + continue; + }; + let Some(build_info) = krate.build_info else { + continue; + }; + + return Some(TargetSpec::ProjectJson(ProjectJsonTargetSpec { + target_kind: build_info.target_kind, + label: build_info.label, + shell_runnables: build_info.shell_runnables, + })); + } ProjectWorkspace::DetachedFiles { .. } => {} } } diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 13a5e8b66431..001aa2b72024 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -773,7 +773,7 @@ pub(crate) fn handle_parent_module( }; let cargo_spec = match TargetSpec::for_file(&snap, file_id)? { Some(TargetSpec::Cargo(it)) => it, - None => return Ok(None), + Some(TargetSpec::ProjectJson(_)) | None => return Ok(None), }; if snap.analysis.crate_root(crate_id)? == file_id { @@ -826,7 +826,6 @@ pub(crate) fn handle_runnables( } if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? { if expect_test { - #[allow(irrefutable_let_patterns)] if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args { runnable.label = format!("{} + expect", runnable.label); r.expect_test = Some(true); @@ -866,6 +865,7 @@ pub(crate) fn handle_runnables( }) } } + Some(TargetSpec::ProjectJson(_)) => {} None => { if !snap.config.linked_or_discovered_projects().is_empty() { res.push(lsp_ext::Runnable { @@ -1771,7 +1771,7 @@ pub(crate) fn handle_open_cargo_toml( let cargo_spec = match TargetSpec::for_file(&snap, file_id)? { Some(TargetSpec::Cargo(it)) => it, - None => return Ok(None), + Some(TargetSpec::ProjectJson(_)) | None => return Ok(None), }; let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml); @@ -2063,7 +2063,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/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs index 41814190ba29..4bf4f290506f 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -431,12 +431,14 @@ pub struct Runnable { #[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)] @@ -456,6 +458,14 @@ pub struct CargoRunnableArgs { pub expect_test: Option, } +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ShellRunnableArgs { + pub program: String, + pub args: Vec, + pub cwd: PathBuf, +} + 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 2e0f735ca74c..501ce6e8124e 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -25,6 +25,7 @@ use crate::{ global_state::GlobalStateSnapshot, line_index::{LineEndings, LineIndex, PositionEncoding}, lsp::{ + ext::ShellRunnableArgs, semantic_tokens::{self, standard_fallback_type}, utils::invalid_params_error, LspError, @@ -1374,6 +1375,27 @@ pub(crate) fn runnable( }), })) } + Some(TargetSpec::ProjectJson(spec)) => { + let label = runnable.label(Some(spec.label.clone())); + 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); @@ -1421,6 +1443,7 @@ pub(crate) fn code_lens( 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(); diff --git a/crates/rust-analyzer/src/target_spec.rs b/crates/rust-analyzer/src/target_spec.rs index d2e518cf3a75..5565562f93f3 100644 --- a/crates/rust-analyzer/src/target_spec.rs +++ b/crates/rust-analyzer/src/target_spec.rs @@ -4,6 +4,8 @@ use std::mem; use cfg::{CfgAtom, CfgExpr}; use ide::{Cancellable, CrateId, FileId, RunnableKind, TestId}; +use project_model::project_json::ShellRunnableArgs; +use project_model::project_json::ShellRunnableKind; use project_model::{CargoFeatures, ManifestPath, TargetKind}; use rustc_hash::FxHashSet; use vfs::AbsPathBuf; @@ -17,6 +19,7 @@ use crate::global_state::GlobalStateSnapshot; #[derive(Clone)] pub(crate) enum TargetSpec { Cargo(CargoTargetSpec), + ProjectJson(ProjectJsonTargetSpec), } impl TargetSpec { @@ -35,6 +38,7 @@ impl TargetSpec { pub(crate) fn target_kind(&self) -> TargetKind { match self { TargetSpec::Cargo(cargo) => cargo.target_kind, + TargetSpec::ProjectJson(project_json) => project_json.target_kind, } } } @@ -55,6 +59,33 @@ pub(crate) struct CargoTargetSpec { pub(crate) features: FxHashSet, } +#[derive(Clone)] +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, ShellRunnableKind::Run) { + return Some(runnable.clone()); + } + } + + None + } + RunnableKind::Test { .. } => None, + RunnableKind::TestMod { .. } => None, + RunnableKind::Bench { .. } => None, + RunnableKind::DocTest { .. } => None, + } + } +} + impl CargoTargetSpec { pub(crate) fn runnable_args( snap: &GlobalStateSnapshot, diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 68637e578df6..be2933df92a9 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@