Skip to content

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

@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.

@chklauser
Copy link
Author

I have updated the implementation to account for changes in nushell since the beginning of the year (--optional) and as a PoC for the auto-updating approach discussed in #5840 .

Tests are still a TODO.

@chklauser chklauser force-pushed the nushell-dynamic-completion branch from bc9b84f to 3ef74cb Compare October 20, 2025 21:42
@chklauser
Copy link
Author

Another update:

# user runs (example: jj)
COMPLETE=nushell jj | save --append --raw $nu.env-path
# env.nu (example: jj)
# Refresh completer integration for jj (must be in env.nu)
do {
  # Search for existing script to avoid duplicates in case autoload dirs change
  let completer_script_name = 'jj-completer.nu'
  let autoload_dir = $nu.user-autoload-dirs
    | where { path join $completer_script_name | path exists }
    | get 0 --optional
    | default ($nu.user-autoload-dirs | get 0 --optional)
  mkdir $autoload_dir

  let completer_path = ($autoload_dir | path join $completer_script_name)
  COMPLETE=nushell _COMPLETE__mode=integration ^r#'/home/USER/.cargo/bin/jj'# | save --raw --force $completer_path
}

and this will maintain ~/.config/nushell/autoload/jj-completer.nu

# Performs the completion for jj
def jj-completer [
    spans: list<string> # The spans that were passed to the external completer closure
]: nothing -> list {
    COMPLETE=nushell ^r#'/home/chris/.cargo/bin/jj'# -- ...$spans | from json
}

@complete jj-completer
def --wrapped jj [...args] {
  ^r#'/home/chris/.cargo/bin/jj'# ...$args
}

}

fn is(&self, name: &str) -> bool {
name.eq_ignore_ascii_case("nushell") || name.eq_ignore_ascii_case("nu")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this is case ignoring and accepting both names?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary motivation was the robustness principle (and I'm well aware of the downsides/criticism of the principle). As a user of nu/nushell, I get the impression that they cannot make up their mind as to what the official name for their shell is. The binary and the repository are called nu, the website, social media accounts, etc. are all called nushell

So the question is: what do we want people to type COMPLETE=nu their-clap-binary (to match the name of the binary/command used to launch nushell) or do we want people to type COMPLETE=nushell their-clap-binary (to match the marketing name for nushell)?

I personally tend towards nushell, but I don't have a very strong opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary motivation was the robustness principle (and I'm well aware of the downsides/criticism of the principle).

In terms of case insensitivity, that is best left to a wider conversation on how we should handle this for all shells while this PR conforms to our existing practices.

As for names, so far we only accept the file stem stored in SHELL

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw that the powershell integration also uses powershell (instead of the binary name pwsh) => I'll change it to just match nushell (case sensitive)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is again related to the SHELL lookup (#4447). If that is incorrect, then that is a bug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To re-iterate differently, since SHELL would end up pointing to nu, that is the name we should use

buf: &mut dyn Write,
) -> Result<(), Error> {
let mode_var = format!("{}", ModeVar(var));
if std::env::var_os(&mode_var).as_ref().map(|x| x.as_os_str())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating an internal environment variable in what is needing to be a public interface is something we'll need to consider more thoroughly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One alternative would be to re-use the COMPLETE variable. We could register two "shells" (or maybe dispatch based on the name used?)

COMPLETE=nushell your-clap-tool generates the auto-update code; COMPLETE=nushell-registration your-clap-tool generates the actual integration code.

Seems a bit more of a hack on the one hand, but it would be safer in the sense that the two are mutually exclusive and in that we don't use some other, undocumented variable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I want to switch all completions to a two-step process for #5668 and have been wondering about doing something like that. This is all unstable so we can change it over time so we don't need to block on it but we will eventually need to figure it out.

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

The user runs
```nushell
COMPLETE=nushell the-clap-tool | save --append --raw $nu.env-path
```
and the dynamic completion will emit an "auto-update" script into
the user's `env.nu`. The auto-update script snippet will
maintain a script in the user's autoload directory that
installs a command-scoped completer.
@chklauser chklauser force-pushed the nushell-dynamic-completion branch from 3ef74cb to 634e750 Compare October 27, 2025 21:43
Comment on lines +15 to +22
// Std
use std::ffi::{OsStr, OsString};
use std::fmt::Display;
use std::io::{Error, Write};
use std::path::Path;

// External
use clap::Command;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't comment the groupings

use clap_complete::Generator;

/// Generate Nushell complete file
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed?

use clap_complete::env::EnvCompleter;

/// Generate integration for dynamic completion in Nushell
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you have all of these traits implemented?

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