From 0819035e10cc1ba6d4d7449078a72b904375b58b Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Wed, 5 Nov 2025 19:23:50 +0100 Subject: [PATCH] feat(completions): Add dynamic completion for packages --- src/bin/cargo/commands/clean.rs | 6 ++- src/bin/cargo/commands/package.rs | 2 + src/bin/cargo/commands/publish.rs | 2 + src/bin/cargo/commands/tree.rs | 7 ++- src/bin/cargo/commands/uninstall.rs | 42 +++++++++++++-- src/cargo/util/command_prelude.rs | 79 ++++++++++++++++++++++++++--- 6 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs index e14ebd379d1..ec2879749ef 100644 --- a/src/bin/cargo/commands/clean.rs +++ b/src/bin/cargo/commands/clean.rs @@ -6,6 +6,7 @@ use cargo::core::global_cache_tracker::GlobalCacheTracker; use cargo::ops::CleanContext; use cargo::ops::{self, CleanOptions}; use cargo::util::print_available_packages; +use clap_complete::ArgValueCandidates; use std::time::Duration; pub fn cli() -> Command { @@ -13,7 +14,10 @@ pub fn cli() -> Command { .about("Remove artifacts that cargo has generated in the past") .arg_doc("Whether or not to clean just the documentation directory") .arg_silent_suggestion() - .arg_package_spec_simple("Package to clean artifacts for") + .arg_package_spec_simple( + "Package to clean artifacts for", + ArgValueCandidates::new(get_pkg_name_candidates), + ) .arg_release("Whether or not to clean release artifacts") .arg_profile("Clean artifacts of the specified profile") .arg_target_triple("Target triple to clean output for") diff --git a/src/bin/cargo/commands/package.rs b/src/bin/cargo/commands/package.rs index aa16989b3d3..b102137171e 100644 --- a/src/bin/cargo/commands/package.rs +++ b/src/bin/cargo/commands/package.rs @@ -3,6 +3,7 @@ use crate::command_prelude::*; use cargo::ops; use cargo::ops::PackageMessageFormat; use cargo::ops::PackageOpts; +use clap_complete::ArgValueCandidates; pub fn cli() -> Command { subcommand("package") @@ -44,6 +45,7 @@ pub fn cli() -> Command { "Package(s) to assemble", "Assemble all packages in the workspace", "Don't assemble specified packages", + ArgValueCandidates::new(get_ws_member_candidates), ) .arg_features() .arg_target_triple("Build for the target triple") diff --git a/src/bin/cargo/commands/publish.rs b/src/bin/cargo/commands/publish.rs index 8819c38e4d9..dfb83ab434f 100644 --- a/src/bin/cargo/commands/publish.rs +++ b/src/bin/cargo/commands/publish.rs @@ -2,6 +2,7 @@ use crate::command_prelude::*; use cargo::ops::{self, PublishOpts}; use cargo_credential::Secret; +use clap_complete::ArgValueCandidates; pub fn cli() -> Command { subcommand("publish") @@ -27,6 +28,7 @@ pub fn cli() -> Command { "Package(s) to publish", "Publish all packages in the workspace", "Don't publish specified packages", + ArgValueCandidates::new(get_ws_member_candidates), ) .arg_features() .arg_parallel() diff --git a/src/bin/cargo/commands/tree.rs b/src/bin/cargo/commands/tree.rs index e54b8b02078..24ddb1d6889 100644 --- a/src/bin/cargo/commands/tree.rs +++ b/src/bin/cargo/commands/tree.rs @@ -7,6 +7,7 @@ use cargo::ops::Packages; use cargo::ops::tree::{self, DisplayDepth, EdgeKind}; use cargo::util::CargoResult; use cargo::util::print_available_packages; +use clap_complete::ArgValueCandidates; use std::collections::HashSet; use std::str::FromStr; @@ -36,7 +37,10 @@ pub fn cli() -> Command { "SPEC", "Invert the tree direction and focus on the given package", ) - .short('i'), + .short('i') + .add(clap_complete::ArgValueCandidates::new( + get_pkg_id_spec_candidates, + )), ) .arg(multi_opt( "prune", @@ -88,6 +92,7 @@ pub fn cli() -> Command { "Package to be used as the root of the tree", "Display the tree for all packages in the workspace", "Exclude specific workspace members", + ArgValueCandidates::new(get_pkg_id_spec_candidates), ) .arg_features() .arg(flag("all-targets", "Deprecated, use --target=all instead").hide(true)) diff --git a/src/bin/cargo/commands/uninstall.rs b/src/bin/cargo/commands/uninstall.rs index 2f764d4549e..3a9478789a3 100644 --- a/src/bin/cargo/commands/uninstall.rs +++ b/src/bin/cargo/commands/uninstall.rs @@ -1,6 +1,9 @@ use crate::command_prelude::*; -use cargo::ops; +use cargo::{CargoResult, core::PackageId, ops}; +use clap_complete::ArgValueCandidates; + +use std::collections::BTreeSet; pub fn cli() -> Command { subcommand("uninstall") @@ -15,7 +18,10 @@ pub fn cli() -> Command { ) .arg(opt("root", "Directory to uninstall packages from").value_name("DIR")) .arg_silent_suggestion() - .arg_package_spec_simple("Package to uninstall") + .arg_package_spec_simple( + "Package to uninstall", + ArgValueCandidates::new(get_installed_package_candidates), + ) .arg( multi_opt("bin", "NAME", "Only uninstall the binary NAME") .help_heading(heading::TARGET_SELECTION), @@ -52,7 +58,7 @@ fn get_installed_crates() -> Vec { fn get_installed_crates_() -> Option> { let mut candidates = Vec::new(); - let gctx = GlobalContext::default().ok()?; + let gctx = new_gctx_for_completions().ok()?; let root = ops::resolve_root(None, &gctx).ok()?; @@ -66,3 +72,33 @@ fn get_installed_crates_() -> Option> { Some(candidates) } + +fn get_installed_package_candidates() -> Vec { + get_installed_packages() + .unwrap_or_default() + .into_iter() + .map(|(pkg, bins)| { + let single_binary = bins.iter().next().take_if(|_| bins.len() == 1); + + let help = if single_binary.is_some_and(|bin| bin == pkg.name().as_str()) { + None + } else { + let binaries = bins.into_iter().collect::>().as_slice().join(", "); + Some(binaries) + }; + + clap_complete::CompletionCandidate::new(pkg.name().as_str()).help(help.map(From::from)) + }) + .collect() +} + +fn get_installed_packages() -> CargoResult)>> { + let gctx = new_gctx_for_completions()?; + let root = ops::resolve_root(None, &gctx)?; + + let tracker = ops::InstallTracker::load(&gctx, &root)?; + Ok(tracker + .all_installed_bins() + .map(|(package_id, bins)| (*package_id, bins.clone())) + .collect()) +} diff --git a/src/cargo/util/command_prelude.rs b/src/cargo/util/command_prelude.rs index 31e3a50b67b..5df0d7588ac 100644 --- a/src/cargo/util/command_prelude.rs +++ b/src/cargo/util/command_prelude.rs @@ -23,11 +23,12 @@ use cargo_util_schemas::manifest::ProfileName; use cargo_util_schemas::manifest::RegistryName; use cargo_util_schemas::manifest::StringOrVec; use clap::builder::UnknownArgumentValueParser; +use clap_complete::ArgValueCandidates; use home::cargo_home_with_cwd; use indexmap::IndexSet; use itertools::Itertools; use semver::Version; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::ffi::{OsStr, OsString}; use std::path::Path; use std::path::PathBuf; @@ -60,7 +61,13 @@ pub trait CommandExt: Sized { all: &'static str, exclude: &'static str, ) -> Self { - self.arg_package_spec_no_all(package, all, exclude)._arg( + self.arg_package_spec_no_all( + package, + all, + exclude, + ArgValueCandidates::new(get_ws_member_candidates), + ) + ._arg( flag("all", "Alias for --workspace (deprecated)") .help_heading(heading::PACKAGE_SELECTION), ) @@ -74,6 +81,7 @@ pub trait CommandExt: Sized { package: &'static str, all: &'static str, exclude: &'static str, + package_completion: ArgValueCandidates, ) -> Self { let unsupported_short_arg = { let value_parser = UnknownArgumentValueParser::suggest_arg("--exclude"); @@ -84,17 +92,28 @@ pub trait CommandExt: Sized { .action(ArgAction::SetTrue) .hide(true) }; - self.arg_package_spec_simple(package) + self.arg_package_spec_simple(package, package_completion) ._arg(flag("workspace", all).help_heading(heading::PACKAGE_SELECTION)) - ._arg(multi_opt("exclude", "SPEC", exclude).help_heading(heading::PACKAGE_SELECTION)) + ._arg( + multi_opt("exclude", "SPEC", exclude) + .help_heading(heading::PACKAGE_SELECTION) + .add(clap_complete::ArgValueCandidates::new( + get_ws_member_candidates, + )), + ) ._arg(unsupported_short_arg) } - fn arg_package_spec_simple(self, package: &'static str) -> Self { + fn arg_package_spec_simple( + self, + package: &'static str, + package_completion: ArgValueCandidates, + ) -> Self { self._arg( optional_multi_opt("package", "SPEC", package) .short('p') - .help_heading(heading::PACKAGE_SELECTION), + .help_heading(heading::PACKAGE_SELECTION) + .add(package_completion), ) } @@ -103,7 +122,10 @@ pub trait CommandExt: Sized { optional_opt("package", package) .short('p') .value_name("SPEC") - .help_heading(heading::PACKAGE_SELECTION), + .help_heading(heading::PACKAGE_SELECTION) + .add(clap_complete::ArgValueCandidates::new(|| { + get_ws_member_candidates() + })), ) } @@ -1313,6 +1335,29 @@ fn get_target_triples_from_rustc() -> CargoResult Vec { + get_ws_member_packages() + .unwrap_or_default() + .into_iter() + .map(|pkg| { + clap_complete::CompletionCandidate::new(pkg.name().as_str()).help( + pkg.manifest() + .metadata() + .description + .to_owned() + .map(From::from), + ) + }) + .collect::>() +} + +fn get_ws_member_packages() -> CargoResult> { + let gctx = new_gctx_for_completions()?; + let ws = Workspace::new(&find_root_manifest_for_wd(gctx.cwd())?, &gctx)?; + let packages = ws.members().map(Clone::clone).collect::>(); + Ok(packages) +} + pub fn get_pkg_id_spec_candidates() -> Vec { let mut candidates = vec![]; @@ -1400,6 +1445,26 @@ pub fn get_pkg_id_spec_candidates() -> Vec { candidates } +pub fn get_pkg_name_candidates() -> Vec { + let packages: BTreeMap<_, _> = get_packages() + .unwrap_or_default() + .into_iter() + .map(|package| { + ( + package.name(), + package.manifest().metadata().description.clone(), + ) + }) + .collect(); + + packages + .into_iter() + .map(|(name, description)| { + clap_complete::CompletionCandidate::new(name.as_str()).help(description.map(From::from)) + }) + .collect() +} + fn get_packages() -> CargoResult> { let gctx = new_gctx_for_completions()?;