From af8b43172cd80bfaab2171e0785a823cf4e6f7c3 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:57:00 +0900 Subject: [PATCH 1/5] fix(env): use the named completer input in env.nu for Nushell 0.116 Nushell 0.116 deprecates a positional completer parameter such as `context`. The env.nu completers now declare `buffer`, which Nushell 0.116 and later bind by name and earlier versions fill positionally. nu-complete vpr removes earlier pipeline elements from `buffer` before it translates `vpr` to `vp run`. Closes #3127 --- .../command_env_nushell/assert_completion.nu | 27 +++++++++++++++++++ .../command_env_nushell/snapshots.toml | 11 ++++++++ .../command_env_nushell_completer_input.md | 15 +++++++++++ .../shell_integration_cwd_templates.md | 11 +++++--- .../vp_global_cli/src/commands/env/setup.rs | 15 ++++++++--- 5 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots/command_env_nushell_completer_input.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu new file mode 100644 index 0000000000..b22e46c13e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu @@ -0,0 +1,27 @@ +# 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", "vpr ", "ls | 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] { + 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 `ls | `. Without the `vpr` translation, fish completes file names. +if ($completions | get 3) != ($completions | get 2) { + error make { msg: $"`ls | vpr ` completed differently from `vpr `, got: ($completions | get 3)" } +} + +print "Nushell completer checks passed" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots.toml index 60ce3e035b..babf9401fe 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots.toml @@ -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"]] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots/command_env_nushell_completer_input.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots/command_env_nushell_completer_input.md new file mode 100644 index 0000000000..05923e5da8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/snapshots/command_env_nushell_completer_input.md @@ -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 +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index 390ff6fe9f..fa2304fccd 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -248,8 +248,8 @@ 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)'" +def "nu-complete vp" [buffer: string] { + let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($buffer)'" ^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} @@ -260,7 +260,12 @@ def "nu-complete vp" [context: string] { } } # Completion logic for vpr (translates context to 'vp run ...') -def "nu-complete vpr" [context: string] { +# Nushell 0.116+ binds `buffer` by name to the line up to the cursor; older releases pass +# only the current command in that slot. The `^vpr` patterns below need the command at +# the start, so drop any earlier pipeline elements. See "Unified custom completions" in +# https://www.nushell.sh/blog/2026-09-26-nushell_v0_116_0.html#unified-custom-completions +def "nu-complete vpr" [buffer: string] { + let context = $buffer | %str replace -r '^(?:.*[|;({])?\s*(?=vpr\b)' '' let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' } else if ($context =~ '^vpr(?\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 176ee3691d..1ec4ceebd5 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1009,8 +1009,8 @@ 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)'" +def "nu-complete vp" [buffer: string] { + let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($buffer)'" ^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} @@ -1021,7 +1021,12 @@ def "nu-complete vp" [context: string] { } } # Completion logic for vpr (translates context to 'vp run ...') -def "nu-complete vpr" [context: string] { +# Nushell 0.116+ binds `buffer` by name to the line up to the cursor; older releases pass +# only the current command in that slot. The `^vpr` patterns below need the command at +# the start, so drop any earlier pipeline elements. See "Unified custom completions" in +# https://www.nushell.sh/blog/2026-09-26-nushell_v0_116_0.html#unified-custom-completions +def "nu-complete vpr" [buffer: string] { + let context = $buffer | %str replace -r '^(?:.*[|;({])?\s*(?=vpr\b)' '' let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' } else if ($context =~ '^vpr(?\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { @@ -1968,6 +1973,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]"#)); + assert!(sigil.contains(r#"def "nu-complete vpr" [buffer: string]"#)); + assert!(!sigil.contains("[context"), "got: {sigil}"); let legacy = render_env_content(EnvShell::Nu, &config, NuSyntax::Legacy); assert!(legacy.contains("| update value {|row|"), "got: {legacy}"); From fb7c58d3bf952e7cbb2ced67a7961810ca45af89 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:51:02 +0900 Subject: [PATCH 2/5] fix(env): read the current command from place in Nushell completers On Nushell 0.116+, buffer holds the whole line up to the cursor. A quote in an earlier command ended the single-quoted fish argument, and the regex that removed earlier pipeline elements failed for multiline input and for delimiters inside quotes. The completers now use place.command, which Nushell parses, and pass the command to fish as an argument. --- .../command_env_nushell/assert_completion.nu | 19 ++++++++++---- .../command_env_nushell/assert_shadowed.nu | 2 ++ .../shell_integration_cwd_templates.md | 22 ++++++++-------- .../vp_global_cli/src/commands/env/setup.rs | 26 +++++++++---------- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu index b22e46c13e..f57d698ab6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu @@ -1,6 +1,13 @@ # 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", "vpr ", "ls | vpr "] +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 @@ -12,16 +19,18 @@ if ($result.stderr | str contains "deprecated") { } let completions = $result.stdout | from json -for index in [0 1] { +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 `ls | `. Without the `vpr` translation, fish completes file names. -if ($completions | get 3) != ($completions | get 2) { - error make { msg: $"`ls | vpr ` completed differently from `vpr `, got: ($completions | get 3)" } +# `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)" } + } } print "Nushell completer checks passed" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_shadowed.nu b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_shadowed.nu index ecba751f54..bafb52d94d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_shadowed.nu +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_shadowed.nu @@ -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" [] { } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index fa2304fccd..509a7607d2 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -248,9 +248,14 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { } # Shell completion for nushell (delegates to fish completions dynamically) -def "nu-complete vp" [buffer: string] { - let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($buffer)'" - ^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 +# Fish gets the command as an argument, so quotes in it cannot end a fish string. +def "nu-complete vp" [buffer: string, place?: any] { + let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } + ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $context | %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)) { @@ -260,12 +265,8 @@ def "nu-complete vp" [buffer: string] { } } # Completion logic for vpr (translates context to 'vp run ...') -# Nushell 0.116+ binds `buffer` by name to the line up to the cursor; older releases pass -# only the current command in that slot. The `^vpr` patterns below need the command at -# the start, so drop any earlier pipeline elements. See "Unified custom completions" in -# https://www.nushell.sh/blog/2026-09-26-nushell_v0_116_0.html#unified-custom-completions -def "nu-complete vpr" [buffer: string] { - let context = $buffer | %str replace -r '^(?:.*[|;({])?\s*(?=vpr\b)' '' +def "nu-complete vpr" [buffer: string, place?: any] { + let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' } else if ($context =~ '^vpr(?\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { @@ -275,8 +276,7 @@ def "nu-complete vpr" [buffer: 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| + ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $modified_context | %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)) { diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 1ec4ceebd5..b581b440fd 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1009,9 +1009,14 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { } # Shell completion for nushell (delegates to fish completions dynamically) -def "nu-complete vp" [buffer: string] { - let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($buffer)'" - ^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 +# Fish gets the command as an argument, so quotes in it cannot end a fish string. +def "nu-complete vp" [buffer: string, place?: any] { + let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } + ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $context | %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)) { @@ -1021,12 +1026,8 @@ def "nu-complete vp" [buffer: string] { } } # Completion logic for vpr (translates context to 'vp run ...') -# Nushell 0.116+ binds `buffer` by name to the line up to the cursor; older releases pass -# only the current command in that slot. The `^vpr` patterns below need the command at -# the start, so drop any earlier pipeline elements. See "Unified custom completions" in -# https://www.nushell.sh/blog/2026-09-26-nushell_v0_116_0.html#unified-custom-completions -def "nu-complete vpr" [buffer: string] { - let context = $buffer | %str replace -r '^(?:.*[|;({])?\s*(?=vpr\b)' '' +def "nu-complete vpr" [buffer: string, place?: any] { + let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' } else if ($context =~ '^vpr(?\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { @@ -1036,8 +1037,7 @@ def "nu-complete vpr" [buffer: 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| + ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $modified_context | %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)) { @@ -1974,8 +1974,8 @@ mod tests { 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]"#)); - assert!(sigil.contains(r#"def "nu-complete vpr" [buffer: string]"#)); + 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); From ab06bbd6b3f1728720c0500e3ca9a9c864a037a1 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:36:36 +0900 Subject: [PATCH 3/5] test(env): cover the legacy Nushell completer input Nushell 0.115 and earlier pass the current command and the cursor position by position. CI has only Nushell 0.116, so call the completers in that form directly and compare the result with the completion of the same line. --- .../command_env_nushell/assert_completion.nu | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu index f57d698ab6..3033e5055e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu @@ -33,4 +33,28 @@ for index in [4 5] { } } +# 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 vp 'vp en' | 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 0] [2 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" From 6d4e1f942d9a79ed11132d96965756c29e3618d9 Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:36:36 +0900 Subject: [PATCH 4/5] refactor(env): share the Nushell completer helpers Move the current-command selection and the fish call into helpers that the `vp` and `vpr` completers share, and document why `place` is `any`. --- .../shell_integration_cwd_templates.md | 25 ++++++++++--------- .../vp_global_cli/src/commands/env/setup.rs | 25 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index 509a7607d2..3d8168ef1f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -252,10 +252,15 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { # 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` 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" [buffer: string, place?: any] { - let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } - ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $context | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row| +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)) { @@ -264,9 +269,12 @@ def "nu-complete vp" [buffer: string, place?: any] { } 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" [buffer: string, place?: any] { - let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } + let context = nu-complete vp current-command $buffer $place let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' } else if ($context =~ '^vpr(?\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { @@ -276,14 +284,7 @@ def "nu-complete vpr" [buffer: string, place?: any] { } else { $context | %str replace -r '^vpr' 'vp run' } - ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $modified_context | %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"] ``` diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index b581b440fd..8a7a74f7ad 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1013,10 +1013,15 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { # 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` 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" [buffer: string, place?: any] { - let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } - ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $context | %from tsv --flexible --noheaders --no-infer | %rename value description | %update value {|row| +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)) { @@ -1025,9 +1030,12 @@ def "nu-complete vp" [buffer: string, place?: any] { } 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" [buffer: string, place?: any] { - let context = if (($place | %describe) starts-with "record") { $place.command | %str join ' ' } else { $buffer } + let context = nu-complete vp current-command $buffer $place let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' } else if ($context =~ '^vpr(?\s+-C=?(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { @@ -1037,14 +1045,7 @@ def "nu-complete vpr" [buffer: string, place?: any] { } else { $context | %str replace -r '^vpr' 'vp run' } - ^fish --command 'VP_COMPLETE=fish command vp | source; complete --do-complete=$argv[1]' $modified_context | %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"] "#; From 6b522aed596ba9917af337e82350d3b885f9076b Mon Sep 17 00:00:00 2001 From: naokihaba <59875779+naokihaba@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:02:41 +0900 Subject: [PATCH 5/5] refactor(env): require place in the Nushell completers Nushell 0.115 always passes two positional values to a completer, and Nushell 0.116 binds place by name. Neither release omits place, so make the parameter required and remove the test call that omitted it. --- .../fixtures/command_env_nushell/assert_completion.nu | 3 +-- .../snapshots/shell_integration_cwd_templates.md | 6 +++--- crates/vp_global_cli/src/commands/env/setup.rs | 10 +++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu index 3033e5055e..3bb22b34f6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_nushell/assert_completion.nu @@ -40,7 +40,6 @@ for index in [4 5] { let legacy_script = "source env.nu [ (nu-complete vp 'vp en' 5 | get value) - (nu-complete vp 'vp en' | get value) (nu-complete vpr 'vpr ' 4 | get value) ] | to json --raw" let legacy_result = ^$nu.current-exe -n -c $legacy_script | complete @@ -49,7 +48,7 @@ if $legacy_result.exit_code != 0 { } let legacy = $legacy_result.stdout | from json -for pair in [[0 0] [1 0] [2 3]] { +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 { diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index 3d8168ef1f..4ffc1c657a 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -254,7 +254,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { # 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` needs Nushell 0.105+, # and every Nushell release must parse the legacy env.nu. -def "nu-complete vp current-command" [buffer: string, place?: any] { +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. @@ -269,11 +269,11 @@ def "nu-complete vp fish" [command: string] { } else {$value} } } -def "nu-complete vp" [buffer: string, place?: any] { +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" [buffer: string, place?: any] { +def "nu-complete vpr" [buffer: string, place: any] { let context = nu-complete vp current-command $buffer $place let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 8a7a74f7ad..90339f898f 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1015,7 +1015,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] { # 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` needs Nushell 0.105+, # and every Nushell release must parse the legacy env.nu. -def "nu-complete vp current-command" [buffer: string, place?: any] { +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. @@ -1030,11 +1030,11 @@ def "nu-complete vp fish" [command: string] { } else {$value} } } -def "nu-complete vp" [buffer: string, place?: any] { +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" [buffer: string, place?: any] { +def "nu-complete vpr" [buffer: string, place: any] { let context = nu-complete vp current-command $buffer $place let modified_context = if ($context =~ '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s') { $context | %str replace -r '^vpr(?\s+-C\s+(?:"[^"]*"|\x27[^\x27]*\x27|\S+))\s' 'vp$cwd run ' @@ -1975,8 +1975,8 @@ mod tests { 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(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);