Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(complete): Add dynamic completion for nushell #5841

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

chklauser
Copy link

@chklauser chklauser commented Dec 11, 2024

Adds an implementation of dynamic completion to clap_complete_nushell under the unstable-dynamic feature flag. Corresponding issue #5840. The issue description has more context and a more complete solution sketch. This PR doesn't implement the full solution sketched there. Ideally, we'd first agree that that solution is the way to go.

With this PR (and the unstable-dynamic feature flag), clap tools can generate a nushell module that offers three commands:

  • complete, which performs the actual completion
  • handles, which asks the module whether it is the correct completer for a particular command line
  • install, which globally registers a completer that falls back to whatever completer was previously installed if handles rejects completing a command line.

The idea is that user's who already have a custom external completer can integrate the generated module's handles and complete commands into their completer.

Everyone else just puts

use my-app-completer.nu
my-app-completer install

into their nushell config.

To test it, I have integrated it into a local build of the relatively complex clap-based tool, JJ (Jujutsu), and it worked very well so far.

TODO

  • Consensus for solution
  • Automated tests

Adds an implementation of dynamic completion to 
`clap_complete_nushell` under the `unstable-dynamic` feature flag.

Nushell currently doesn't have a good story for adding what nushell
calls "external completers" in a modular fashion. Users can define
a single, global external completer. If they wish to combine multiple
external completers, they have to manually write a sort of 
meta-completer that dispatches to the individual completers based
on the first argument (the binary).

This PR generates a nushell module that offers three commands:
 - `complete`, which performs the actual completion
 - `handles`, which asks the module whether it is the correct 
    completer for a particular command line
 - `install`, which globally registers a completer that falls back
    to whatever completer was previously installed if `handles`
    rejects completing a command line.

The idea is that user's who already have a custom external completer
can integrate the generated module's `handles` and `complete` commands
into their completer.

Everyone else just puts
```nushell
use my-app-completer.nu
my-app-completer install
```
into their nushell config.
@chklauser
Copy link
Author

I do have some questions:

  1. Is feat (minor version bump) OK?
  2. This is the first time I have had any contact with shell completion. What would you expect in terms of automated tests? I'm having a hard time telling the tests that cover the "static" completions apart from the ones that cover the dynamic completion shell integrations.

@epage
Copy link
Member

epage commented Dec 12, 2024

Is feat (minor version bump) OK?

feat is right though we don't bump minor on features

What would you expect in terms of automated tests?

For dynamic completions, we focus on the shell integration

  • Does it work at all
  • What sort order is used (ours or the shells)
  • How does escaping work

etc

e.g. see

#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn register_dynamic_env() {
common::register_example::<RuntimeBuilder>("dynamic-env", "exhaustive");
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn complete_dynamic_env_toplevel() {
if !common::has_command(CMD) {
return;
}
let term = completest::Term::new();
let mut runtime = common::load_runtime::<RuntimeBuilder>("dynamic-env", "exhaustive");
let input = "exhaustive \t\t";
let expected = snapbox::str![[r#"
%
action value last hint --global --help
quote pacman alias help --generate --version
"#]];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn complete_dynamic_env_quoted_help() {
if !common::has_command(CMD) {
return;
}
let term = completest::Term::new();
let mut runtime = common::load_runtime::<RuntimeBuilder>("dynamic-env", "exhaustive");
let input = "exhaustive quote \t\t";
let expected = snapbox::str![[r#"
%
cmd-single-quotes cmd-backslash escape-help --global --backslash --choice
cmd-double-quotes cmd-brackets help --double-quotes --brackets --help
cmd-backticks cmd-expansions --single-quotes --backticks --expansions --version
"#]];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn complete_dynamic_env_option_value() {
if !common::has_command(CMD) {
return;
}
let term = completest::Term::new();
let mut runtime = common::load_runtime::<RuntimeBuilder>("dynamic-env", "exhaustive");
let input = "exhaustive action --choice=\t\t";
let expected = snapbox::str!["% "];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
let input = "exhaustive action --choice=f\t";
let expected = snapbox::str!["exhaustive action --choice=f % exhaustive action --choice=f"];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
}
#[test]
#[cfg(all(unix, feature = "unstable-dynamic"))]
#[cfg(feature = "unstable-shell-tests")]
fn complete_dynamic_env_quoted_value() {
if !common::has_command(CMD) {
return;
}
let term = completest::Term::new();
let mut runtime = common::load_runtime::<RuntimeBuilder>("dynamic-env", "exhaustive");
let input = "exhaustive quote --choice \t\t";
let expected = snapbox::str![[r#"
%
another shell bash fish zsh
"#]];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
let input = "exhaustive quote --choice an\t";
let expected =
snapbox::str!["exhaustive quote --choice an % exhaustive quote --choice another shell "];
let actual = runtime.complete(input, &term).unwrap();
assert_data_eq!(actual, expected);
}

Note that I'm going to focus first on the issue to see if there is any points there ti work out before coming back to the PR to review the implementation.

@chklauser
Copy link
Author

re:tests: Thanks! I'll have a look at these. Getting them up and running for nushell should be relatively independent of the concrete implementation.

Note that I'm going to focus first on the issue to see if there is any points there ti work out before coming back to the PR to review the implementation.

Yep, let's nail down the conceptual aspects first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants