Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Nushell 0.116 deprecates a positional completer input such as `context` (#3127).
# Nushell writes the warning to stderr, so complete in a child process.
let lines = [
"vp en",
"ls | vp en",
"echo \"it's okay\" | vp en",
"vpr ",
"ls | vpr ",
"do {\n let x = 1\n vpr ",
]
let script = $"source env.nu
($lines | to nuon) | each {|line| $line | commandline complete } | to json --raw"
let result = ^$nu.current-exe -n -c $script | complete
if $result.exit_code != 0 {
error make { msg: $"completion failed: ($result.stderr)" }
}
if ($result.stderr | str contains "deprecated") {
error make { msg: $"completion printed a deprecation warning: ($result.stderr)" }
}

let completions = $result.stdout | from json
for index in [0 1 2] {
let values = $completions | get $index | each {|value| $value | str trim }
if "env" not-in $values {
error make { msg: $"`($lines | get $index)` did not complete `env`, got: ($values)" }
}
}

# `buffer` includes the earlier commands. Without the `vpr` translation, fish completes file names.
for index in [4 5] {
if ($completions | get $index) != ($completions | get 3) {
error make { msg: $"`($lines | get $index)` completed differently from `vpr `, got: ($completions | get $index)" }
}
}

# Nushell 0.115 and earlier pass the current command and the cursor position by position.
# CI has only a newer Nushell, so call the completers in that form directly.
# An empty command makes fish list every command, which also includes `env`, so compare
# each result with the completion of the same line above.
let legacy_script = "source env.nu
[
(nu-complete vp 'vp en' 5 | get value)
(nu-complete vpr 'vpr ' 4 | get value)
] | to json --raw"
let legacy_result = ^$nu.current-exe -n -c $legacy_script | complete
if $legacy_result.exit_code != 0 {
error make { msg: $"legacy completion failed: ($legacy_result.stderr)" }
}

let legacy = $legacy_result.stdout | from json
for pair in [[0 0] [1 3]] {
let expected = $completions | get $pair.1 | each {|value| $value | str trim } | sort
let got = $legacy | get $pair.0 | each {|value| $value | str trim } | sort
if $got != $expected {
error make { msg: $"legacy `($lines | get $pair.1)` completed differently, got: ($got)" }
}
}

print "Nushell completer checks passed"
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def prepend [] { }
def append [] { }
def "str starts-with" [] { }
def "str replace" [] { }
def "str join" [] { }
def describe [] { }
def "path expand" [] { }
def "path exists" [] { }
def "from tsv" [] { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ steps = [
{ argv = ["vpt", "cp", "assert_shadowed.nu", "home/assert_shadowed.nu"], snapshot = false },
{ argv = ["nu", "assert_shadowed.nu"], cwd = "home", comment = "env.nu loads and runs `vp env use` when user commands shadow the built-ins it calls (#3092)", envs = [["VP_HOME", "${workspace}/home"]] },
]

[[case]]
name = "command_env_nushell_completer_input"
vp = "global"
skip-platforms = ["windows"]
requires = ["nu", "fish"]
steps = [
{ argv = ["vp", "env", "setup", "--refresh"], envs = [["VP_HOME", "${workspace}/home"]], snapshot = false },
{ argv = ["vpt", "cp", "assert_completion.nu", "home/assert_completion.nu"], snapshot = false },
{ argv = ["nu", "assert_completion.nu"], cwd = "home", comment = "env.nu completers complete `vp` after a pipeline and do not use the positional input that Nushell 0.116 deprecates (#3127)", envs = [["VP_HOME", "${workspace}/home"]] },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# command_env_nushell_completer_input

## `VP_HOME=${workspace}/home vp env setup --refresh`


## `vpt cp assert_completion.nu home/assert_completion.nu`


## `cd home && VP_HOME=${workspace}/home nu assert_completion.nu`

env.nu completers complete `vp` after a pipeline and do not use the positional input that Nushell 0.116 deprecates (#3127)

```
Nushell completer checks passed
```
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,19 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] {
}

# Shell completion for nushell (delegates to fish completions dynamically)
def "nu-complete vp" [context: string] {
let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($context)'"
^fish --command $fish_cmd | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row|
# Nushell 0.116+ binds `buffer` and `place` by name: `buffer` is the whole line up to the cursor,
# and `place.command` is the words of the current command up to the cursor. Older releases pass
# the current command in `buffer` and the cursor position in `place`. See "Unified custom
# completions" in https://www.nushell.sh/blog/2026-09-26-nushell_v0_116_0.html#unified-custom-completions
# `place` is `any` because it is an int or a record. `oneof<int, record>` needs Nushell 0.105+,
# and every Nushell release must parse the legacy env.nu.
def "nu-complete vp current-command" [buffer: string, place: any] {
if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer }
}
# Fish gets the command as an argument, so quotes in it cannot end a fish string.
def "nu-complete vp fish" [command: string] {
let fish_script = 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]'
^fish --command $fish_script $command | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row|
let value = $row.value
let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | %any {$in in $value}
if ($need_quote and ($value | %path exists)) {
Expand All @@ -259,8 +269,12 @@ def "nu-complete vp" [context: string] {
} else {$value}
}
}
def "nu-complete vp" [buffer: string, place: any] {
nu-complete vp fish (nu-complete vp current-command $buffer $place)
}
# Completion logic for vpr (translates context to 'vp run ...')
def "nu-complete vpr" [context: string] {
def "nu-complete vpr" [buffer: string, place: any] {
let context = nu-complete vp current-command $buffer $place
let modified_context = if ($context =~ '^vpr(?<cwd>\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') {
$context | %str replace -r '^vpr(?<cwd>\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run '
} else if ($context =~ '^vpr(?<cwd>\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') {
Expand All @@ -270,15 +284,7 @@ def "nu-complete vpr" [context: string] {
} else {
$context | %str replace -r '^vpr' 'vp run'
}
let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($modified_context)'"
^fish --command $fish_cmd | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row|
let value = $row.value
let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | %any {$in in $value}
if ($need_quote and ($value | %path exists)) {
let expanded_path = if ($value starts-with ~) {$value | %path expand --no-symlink} else {$value}
$'"($expanded_path | %str replace --all "\"" "\\\"")"'
} else {$value}
}
nu-complete vp fish $modified_context
}
export extern "vpr" [...args: string@"nu-complete vpr"]
```
Expand Down
36 changes: 23 additions & 13 deletions crates/vp_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,9 +1009,19 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] {
}

# Shell completion for nushell (delegates to fish completions dynamically)
def "nu-complete vp" [context: string] {
let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($context)'"
^fish --command $fish_cmd | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row|
# Nushell 0.116+ binds `buffer` and `place` by name: `buffer` is the whole line up to the cursor,
# and `place.command` is the words of the current command up to the cursor. Older releases pass
# the current command in `buffer` and the cursor position in `place`. See "Unified custom
# completions" in https://www.nushell.sh/blog/2026-09-26-nushell_v0_116_0.html#unified-custom-completions
# `place` is `any` because it is an int or a record. `oneof<int, record>` needs Nushell 0.105+,
# and every Nushell release must parse the legacy env.nu.
def "nu-complete vp current-command" [buffer: string, place: any] {
if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer }
}
# Fish gets the command as an argument, so quotes in it cannot end a fish string.
def "nu-complete vp fish" [command: string] {
let fish_script = 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]'
^fish --command $fish_script $command | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row|
let value = $row.value
let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | %any {$in in $value}
if ($need_quote and ($value | %path exists)) {
Expand All @@ -1020,8 +1030,12 @@ def "nu-complete vp" [context: string] {
} else {$value}
}
}
def "nu-complete vp" [buffer: string, place: any] {
nu-complete vp fish (nu-complete vp current-command $buffer $place)
}
# Completion logic for vpr (translates context to 'vp run ...')
def "nu-complete vpr" [context: string] {
def "nu-complete vpr" [buffer: string, place: any] {
let context = nu-complete vp current-command $buffer $place
let modified_context = if ($context =~ '^vpr(?<cwd>\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') {
$context | %str replace -r '^vpr(?<cwd>\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run '
} else if ($context =~ '^vpr(?<cwd>\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') {
Expand All @@ -1031,15 +1045,7 @@ def "nu-complete vpr" [context: string] {
} else {
$context | %str replace -r '^vpr' 'vp run'
}
let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($modified_context)'"
^fish --command $fish_cmd | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row|
let value = $row.value
let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | %any {$in in $value}
if ($need_quote and ($value | %path exists)) {
let expanded_path = if ($value starts-with ~) {$value | %path expand --no-symlink} else {$value}
$'"($expanded_path | %str replace --all "\"" "\\\"")"'
} else {$value}
}
nu-complete vp fish $modified_context
}
export extern "vpr" [...args: string@"nu-complete vpr"]
"#;
Expand Down Expand Up @@ -1968,6 +1974,10 @@ mod tests {
assert!(sigil.contains("| %update value {|row|"), "got: {sigil}");
assert!(sigil.contains(r#"("/tmp/vp%home" | %path expand"#), "got: {sigil}");
assert!(sigil.contains("^fish --command"), "got: {sigil}");
// Completers must not cause the Nushell 0.116 positional-input warning (#3127).
assert!(sigil.contains(r#"def "nu-complete vp" [buffer: string, place: any]"#));
assert!(sigil.contains(r#"def "nu-complete vpr" [buffer: string, place: any]"#));
assert!(!sigil.contains("[context"), "got: {sigil}");

let legacy = render_env_content(EnvShell::Nu, &config, NuSyntax::Legacy);
assert!(legacy.contains("| update value {|row|"), "got: {legacy}");
Expand Down
Loading