Skip to content

Commit e7ca9be

Browse files
committedSep 10, 2024·
Auto merge of #14493 - shannmu:dynamic_switch, r=epage
feat: Add native comlpetion with CompleteEnv under the nightly ### What does this PR try to resolve? Related issue #6645 Tracking issue #14520 This PR is the first step to move cargo shell completions to native completions by using `clap_complete` crate. It makes users could complete cargo subcommand and flags. By using `clap_complete` crate, we could extend the supported shells to Bash, Zsh, Elvish, Fish, and PowerShell. However, at the current stage, the support for PowerShell in `clap_complete` is not fully developed. See clap-rs/clap#3166 to get more context about what features `clap_complete` has supported. ### How to test and review this PR? 1. Build a test environment, including the necessary short completion scripts, and the `complete` function to start an interactive shell with the help of a pty device and obtain completion results. 2. Simply test the completion results of subcommands in bash, zsh, fish, elvish.
2 parents bd5f32b + 7b0b977 commit e7ca9be

File tree

7 files changed

+652
-223
lines changed

7 files changed

+652
-223
lines changed
 

‎.github/workflows/main.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ jobs:
173173
- run: rustup target add ${{ matrix.other }}
174174
- run: rustup component add rustc-dev llvm-tools-preview rust-docs
175175
if: startsWith(matrix.rust, 'nightly')
176+
# Install fish, zsh, and elvish only on Ubuntu systems
177+
- name: Install fish, zsh, and elvish on Ubuntu
178+
run: sudo apt update -y && sudo apt install fish zsh elvish -y
179+
if: matrix.os == 'ubuntu-latest'
180+
- name: Install fish, elvish on macOS
181+
run: brew install fish elvish
182+
if: matrix.os == 'macos-14' || matrix.os == 'macos-13'
176183
- run: sudo apt update -y && sudo apt install lldb gcc-multilib libsecret-1-0 libsecret-1-dev -y
177184
if: matrix.os == 'ubuntu-latest'
178185
- run: rustup component add rustfmt || echo "rustfmt not available"
@@ -227,6 +234,7 @@ jobs:
227234
- run: rustup update --no-self-update stable && rustup default stable
228235
- run: rustup target add i686-unknown-linux-gnu
229236
- run: sudo apt update -y && sudo apt install gcc-multilib libsecret-1-0 libsecret-1-dev -y
237+
- run: sudo apt update -y && sudo apt install fish zsh elvish -y
230238
- run: rustup component add rustfmt || echo "rustfmt not available"
231239
- run: cargo test -p cargo
232240
env:

‎Cargo.lock

Lines changed: 406 additions & 223 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ cargo-util = { version = "0.2.14", path = "crates/cargo-util" }
3636
cargo-util-schemas = { version = "0.6.0", path = "crates/cargo-util-schemas" }
3737
cargo_metadata = "0.18.1"
3838
clap = "4.5.11"
39+
clap_complete = { version = "4.5.24", features = ["unstable-dynamic"] }
3940
color-print = "0.3.6"
41+
completest-pty = "0.5.3"
4042
core-foundation = { version = "0.10.0", features = ["mac_os_10_7_support"] }
4143
crates-io = { version = "0.40.4", path = "crates/crates-io" }
4244
criterion = { version = "0.5.1", features = ["html_reports"] }
@@ -153,6 +155,7 @@ cargo-platform.workspace = true
153155
cargo-util-schemas.workspace = true
154156
cargo-util.workspace = true
155157
clap = { workspace = true, features = ["wrap_help"] }
158+
clap_complete.workspace = true
156159
color-print.workspace = true
157160
crates-io.workspace = true
158161
curl = { workspace = true, features = ["http2"] }
@@ -239,6 +242,7 @@ features = [
239242
[dev-dependencies]
240243
annotate-snippets = { workspace = true, features = ["testing-colors"] }
241244
cargo-test-support.workspace = true
245+
completest-pty.workspace = true
242246
gix = { workspace = true, features = ["revision"] }
243247
same-file.workspace = true
244248
snapbox.workspace = true

‎src/bin/cargo/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![allow(clippy::self_named_module_files)] // false positive in `commands/build.rs`
22

3+
use cargo::core::features;
34
use cargo::core::shell::Shell;
45
use cargo::util::network::http::http_handle;
56
use cargo::util::network::http::needs_custom_http_transport;
@@ -28,6 +29,13 @@ fn main() {
2829
}
2930
};
3031

32+
let nightly_features_allowed = matches!(&*features::channel(), "nightly" | "dev");
33+
if nightly_features_allowed {
34+
clap_complete::CompleteEnv::with_factory(|| cli::cli(&mut gctx))
35+
.var("CARGO_COMPLETE")
36+
.complete();
37+
}
38+
3139
let result = if let Some(lock_addr) = cargo::ops::fix_get_proxy_lock_addr() {
3240
cargo::ops::fix_exec_rustc(&gctx, &lock_addr).map_err(|e| CliError::from(e))
3341
} else {

‎src/doc/src/reference/unstable.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Each new feature described below should explain how to use it.
117117
* [script](#script) --- Enable support for single-file `.rs` packages.
118118
* [lockfile-path](#lockfile-path) --- Allows to specify a path to lockfile other than the default path `<workspace_root>/Cargo.lock`.
119119
* [package-workspace](#package-workspace) --- Allows for packaging and publishing multiple crates in a workspace.
120+
* [native-completions](#native-completions) --- Move cargo shell completions to native completions.
120121

121122
## allow-features
122123

@@ -1686,6 +1687,30 @@ cargo +nightly -Zpackage-workspace --registry=my-registry package -p foo -p dep
16861687
cargo +nightly -Zpackage-workspace --index=https://example.com package -p foo -p dep
16871688
```
16881689

1690+
## native-completions
1691+
* Original Issue: [#6645](https://github.com/rust-lang/cargo/issues/6645)
1692+
* Tracking Issue: [#14520](https://github.com/rust-lang/cargo/issues/14520)
1693+
1694+
This feature moves the handwritten completion scripts to Rust native, making it
1695+
easier for us to add, extend and test new completions. This feature is enabled with the
1696+
nightly channel, without requiring additional `-Z` options.
1697+
1698+
### How to use native-completions feature:
1699+
- bash:
1700+
Add `source <(CARGO_COMPLETE=bash cargo)` to your .bashrc.
1701+
1702+
- zsh:
1703+
Add `source <(CARGO_COMPLETE=zsh cargo)` to your .zshrc.
1704+
1705+
- fish:
1706+
Add `source (CARGO_COMPLETE=fish cargo | psub)` to `$XDG_CONFIG_HOME/fish/completions/cargo.fish`
1707+
1708+
- elvish:
1709+
Add `eval (E:CARGO_COMPLETE=elvish cargo | slurp)` to `$XDG_CONFIG_HOME/elvish/rc.elv`
1710+
1711+
- powershell:
1712+
Add `CARGO_COMPLETE=powershell cargo | Invoke-Expression` to `$PROFILE`.
1713+
16891714
# Stabilized and removed features
16901715

16911716
## Compile progress

‎tests/testsuite/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ mod rustflags;
166166
mod rustup;
167167
mod script;
168168
mod search;
169+
mod shell_completions;
169170
mod shell_quoting;
170171
mod source_replacement;
171172
mod ssh;

‎tests/testsuite/shell_completions.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#![cfg(unix)]
2+
3+
use cargo_test_support::cargo_test;
4+
use completest_pty::Runtime;
5+
use snapbox::assert_data_eq;
6+
7+
#[cargo_test]
8+
fn bash() {
9+
// HACK: At least on CI, bash is not working on macOS
10+
if cfg!(target_os = "macos") {
11+
return;
12+
}
13+
14+
let input = "cargo \t\t";
15+
let expected = snapbox::str![
16+
"%
17+
--version --help check install read-manifest update
18+
--list -V clean locate-project remove vendor
19+
--explain -v config login report verify-project
20+
--verbose -q doc logout run version
21+
--quiet -C fetch metadata rustc yank
22+
--color -Z fix new rustdoc
23+
--locked -h generate-lockfile owner search
24+
--offline add help package test
25+
--frozen bench info pkgid tree
26+
--config build init publish uninstall "
27+
];
28+
let actual = complete(input, "bash");
29+
assert_data_eq!(actual, expected);
30+
}
31+
32+
#[cargo_test]
33+
fn elvish() {
34+
// HACK: At least on CI, elvish is not working on macOS
35+
if cfg!(target_os = "macos") {
36+
return;
37+
}
38+
39+
let input = "cargo \t\t";
40+
let expected = snapbox::str![
41+
"% cargo --config
42+
COMPLETING argument
43+
--color --version check install read-manifest update
44+
--config -C clean locate-project remove vendor
45+
--explain -V config login report verify-project
46+
--frozen -Z doc logout run version
47+
--help -h fetch metadata rustc yank
48+
--list -q fix new rustdoc
49+
--locked -v generate-lockfile owner search
50+
--offline add help package test
51+
--quiet bench info pkgid tree
52+
--verbose build init publish uninstall "
53+
];
54+
let actual = complete(input, "elvish");
55+
assert_data_eq!(actual, expected);
56+
}
57+
58+
#[cargo_test]
59+
fn fish() {
60+
// HACK: At least on CI, fish is not working on macOS
61+
if cfg!(target_os = "macos") {
62+
return;
63+
}
64+
65+
let input = "cargo \t\t";
66+
let expected = snapbox::str![
67+
"% cargo
68+
--version (Print version info and exit)
69+
--list (List installed commands)
70+
--explain (Provide a detailed explanation of a rustc error message)
71+
--verbose (Use verbose output (-vv very verbose/build.rs output))
72+
--quiet (Do not print cargo log messages)
73+
--color (Coloring: auto, always, never)
74+
--locked (Assert that `Cargo.lock` will remain unchanged)
75+
--offline (Run without accessing the network)
76+
--frozen (Equivalent to specifying both --locked and --offline)
77+
--config (Override a configuration value)
78+
--help (Print help)
79+
-V (Print version info and exit)
80+
-v (Use verbose output (-vv very verbose/build.rs output))
81+
-q (Do not print cargo log messages)
82+
-C (Change to DIRECTORY before doing anything (nightly-only))
83+
-Z (Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details)
84+
-h (Print help)
85+
add (Add dependencies to a Cargo.toml manifest file)
86+
bench (Execute all benchmarks of a local package)
87+
build (Compile a local package and all of its dependencies)
88+
check (Check a local package and all of its dependencies for errors)
89+
clean (Remove artifacts that cargo has generated in the past)
90+
config (Inspect configuration values)
91+
doc (Build a package's documentation)
92+
fetch (Fetch dependencies of a package from the network)
93+
fix (Automatically fix lint warnings reported by rustc)
94+
generate-lockfile (Generate the lockfile for a package)
95+
help (Displays help for a cargo subcommand)
96+
info (Display information about a package in the registry)
97+
init (Create a new cargo package in an existing directory)
98+
install (Install a Rust binary)
99+
locate-project (Print a JSON representation of a Cargo.toml file's location)
100+
login (Log in to a registry.)
101+
logout (Remove an API token from the registry locally)
102+
metadata (Output the resolved dependencies of a package, the concrete used versions including overrides, in machine-r…)
103+
new (Create a new cargo package at <path>)
104+
owner (Manage the owners of a crate on the registry)
105+
package (Assemble the local package into a distributable tarball)
106+
pkgid (Print a fully qualified package specification)
107+
publish (Upload a package to the registry)
108+
read-manifest (Print a JSON representation of a Cargo.toml manifest.)
109+
remove (Remove dependencies from a Cargo.toml manifest file)
110+
report (Generate and display various kinds of reports)
111+
run (Run a binary or example of the local package)
112+
rustc (Compile a package, and pass extra options to the compiler)
113+
rustdoc (Build a package's documentation, using specified custom flags.)
114+
search (Search packages in the registry. Default registry is crates.io)
115+
test (Execute all unit and integration tests and build examples of a local package)
116+
tree (Display a tree visualization of a dependency graph)
117+
uninstall (Remove a Rust binary)
118+
update (Update dependencies as recorded in the local lock file)
119+
vendor (Vendor all dependencies for a project locally)
120+
verify-project (Check correctness of crate manifest)
121+
version (Show version information)
122+
yank (Remove a pushed crate from the index)"];
123+
124+
let actual = complete(input, "fish");
125+
assert_data_eq!(actual, expected);
126+
}
127+
128+
#[cargo_test]
129+
fn zsh() {
130+
let input = "cargo \t\t";
131+
let expected = snapbox::str![
132+
"% cargo
133+
--color --version check install read-manifest update
134+
--config -C clean locate-project remove vendor
135+
--explain -V config login report verify-project
136+
--frozen -Z doc logout run version
137+
--help -h fetch metadata rustc yank
138+
--list -q fix new rustdoc
139+
--locked -v generate-lockfile owner search
140+
--offline add help package test
141+
--quiet bench info pkgid tree
142+
--verbose build init publish uninstall "
143+
];
144+
let actual = complete(input, "zsh");
145+
assert_data_eq!(actual, expected);
146+
}
147+
148+
fn complete(input: &str, shell: &str) -> String {
149+
let shell = shell.into();
150+
151+
// Load the runtime
152+
let mut runtime = match shell {
153+
"bash" => load_runtime::<completest_pty::BashRuntimeBuilder>("bash"),
154+
"elvish" => load_runtime::<completest_pty::ElvishRuntimeBuilder>("elvish"),
155+
"fish" => load_runtime::<completest_pty::FishRuntimeBuilder>("fish"),
156+
"zsh" => load_runtime::<completest_pty::ZshRuntimeBuilder>("zsh"),
157+
_ => panic!("Unsupported shell: {}", shell),
158+
};
159+
160+
// Exec the completion
161+
let term = completest_pty::Term::new();
162+
let actual = runtime.complete(input, &term).unwrap();
163+
164+
actual
165+
}
166+
167+
// Return the scratch directory to keep it not being dropped
168+
fn load_runtime<R: completest_pty::RuntimeBuilder>(shell: &str) -> Box<dyn completest_pty::Runtime>
169+
where
170+
<R as completest_pty::RuntimeBuilder>::Runtime: 'static,
171+
{
172+
let home = cargo_test_support::paths::home();
173+
174+
let bin_path = cargo_test_support::cargo_exe();
175+
let bin_root = bin_path.parent().unwrap().to_owned();
176+
177+
let mut runtime = Box::new(R::new(bin_root, home).unwrap());
178+
179+
match shell {
180+
"bash" => runtime
181+
.register("", "source <(CARGO_COMPLETE=bash cargo)")
182+
.unwrap(),
183+
"elvish" => runtime
184+
.register("", "eval (E:CARGO_COMPLETE=elvish cargo | slurp)")
185+
.unwrap(),
186+
"fish" => runtime
187+
.register("cargo", "source (CARGO_COMPLETE=fish cargo | psub)")
188+
.unwrap(),
189+
"zsh" => runtime
190+
.register(
191+
"cargo",
192+
"#compdef cargo
193+
source <(CARGO_COMPLETE=zsh cargo)",
194+
)
195+
.unwrap(),
196+
_ => panic!("Unsupported shell: {}", shell),
197+
}
198+
199+
runtime
200+
}

0 commit comments

Comments
 (0)
Please sign in to comment.