261017.1 · 55c122f1a0e1340546c8709682bea5561fe0e8fd
fix(nushell): adapt the completer to the 0.116 contract (#4934)
* fix(nushell): adapt the completer to the 0.116 contract
Nushell 0.116 runs an external completer on a background worker without the terminal unless the registered closure reaches a command carrying the @interactive attribute. Interactivity is seen through the closure to the command it calls, so wrap the completion logic in an @interactive entry point and register a closure that ends with the call to it.
Read the named `place` input instead of the deprecated positional `spans`; $place.command carries the same token list.
An @interactive completer cannot decline with null (null reads as a dismissed picker), so request the fallback explicitly when the `**` trigger is absent, keeping plain file completion working. The previous external completer is still called when the trigger is absent.
The script now requires Nushell 0.116+, since @interactive is a parse error on older versions.
Fixes #4932
* fix(nushell): retry the previous completer with the legacy spans input
The fallback to the previously configured external completer always handed it the new `place` input. A completer that still declares the legacy `spans` parameter errors on a record, so the fallback silently produced no completions for pre-0.116 completers (e.g. older Carapace installs).
Try `place` first, then retry with the token list, which is what the old positional bridge handed such completers.
* docs(nushell): note the 0.116 requirement and load order
Requires Nushell 0.116.1+, where a completion can take the terminal through the @interactive attribute, and the wrapper captures the external completer present at load time.
reading declarations
Changes to shell/completion.nu. All files
@@ -8,6 +8,15 @@ # An implementation of completion.nu # This loads FZF as a Nushell External Completer # https://www.nushell.sh/cookbook/external_completers.html+#+# Requires Nushell 0.116.0 or newer.+#+# On 0.116+, the registered completer closure must reach a command carrying the+# `@interactive` attribute: interactivity is seen through the closure to that+# command (a closure cannot carry the attribute itself), and that is what makes+# the completion run on the REPL thread with the terminal, so fzf can take it.+# A closure that does not reach such a command runs on a background worker+# without the terminal, where fzf cannot run. # --- Default Environment Variables ---@@ -367,9 +376,9 @@ def _fzf_complete_kill [query: string] { # --- Main FZF External Completer --- -# This function is registered with Nushell's external completion system.-# It gets called when Tab is pressed.-let fzf_external_completer = {|spans|+# Main completion logic. It receives the token list of the command being+# completed (the legacy `spans`). Called directly, so no attribute is needed.+def __fzf_completer_logic [spans] { let trigger: string = $env.FZF_COMPLETION_TRIGGER? | default '**' if ($trigger | is-empty) { return null } # Cannot work with empty trigger@@ -446,6 +455,40 @@ let fzf_external_completer = {|spans| } } +# Nushell 0.116+ entry point. The registered closure must END with a call to+# this command for the completion to run on the REPL thread with the terminal.+#+# It declares `place` (a named input) rather than the legacy `spans`, so the+# completer never goes through the deprecated positional bridge. `$place.command`+# is the token list of the command being completed, the same value the old+# `spans` held (with a trailing "" when the cursor sits on an empty slot).+@interactive+def __fzf_external_completer [place] {+ let result = (__fzf_completer_logic $place.command)+ if $result != null {+ $result+ } else {+ # Fall back to the previously configured external completer, if any. It is+ # handed the new `place` input first. A completer that still declares the+ # legacy `spans` parameter errors on a record, so it is retried with the+ # token list, which is what the old positional bridge handed it.+ let previous = ($env.__fzf_previous_completer? | default null)+ let previous_result = if $previous != null {+ try {+ do $previous $place+ } catch {+ try { do $previous $place.command } catch { null }+ }+ } else {+ null+ }+ # An @interactive completer cannot decline with null: null reads as a+ # dismissed picker with an empty, final answer. Request the fallback+ # explicitly so file completion still runs when the trigger is absent.+ if $previous_result != null { $previous_result } else { {completions: [], fallback: true} }+ }+}+ # --- WRAPPER AND REGISTRATION --- # Guard against re-sourcing: wrapping the completer multiple times would@@ -455,25 +498,15 @@ if ($env.__fzf_completer_registered? | default false) != true { # Get the currently configured external completer, if any exists let previous_external_completer = $env.config? | get completions? | get external? | get completer? - # Define the new wrapper completer- let fzf_wrapper_completer = {|spans|- # 1. Try the FZF completer logic first- let fzf_result = do $fzf_external_completer $spans+ # Stash it for `__fzf_external_completer`: a def cannot capture a local+ # variable, and the fallback to the previous completer happens inside it.+ $env.__fzf_previous_completer = $previous_external_completer - # 2. If FZF returned a result (a list, even an empty one), return it.- # `null` means FZF didn't handle it because the trigger wasn't present.- if $fzf_result != null {- $fzf_result- } else {- # 3. FZF didn't handle it, so call the previous completer (if it exists).- if $previous_external_completer != null {- do $previous_external_completer $spans- } else {- # 4. No previous completer, and FZF didn't handle it. Return null.- null- }- }- }+ # The registered closure must END with a call to an `@interactive` command so+ # that Nushell 0.116+ runs the completion on the REPL thread with the terminal+ # (it inspects the last pipeline element of the closure). Declaring `place`+ # instead of `spans` keeps it off the deprecated positional bridge.+ let fzf_wrapper_completer = {|place| __fzf_external_completer $place } # Register the new wrapper completer # This ensures external completions are enabled and sets our wrapper.