diff --git a/shell/completion.nu b/shell/completion.nu index 3b04af44..4aa98b57 100644 --- a/shell/completion.nu +++ b/shell/completion.nu @@ -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,29 @@ 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. + let previous = ($env.__fzf_previous_completer? | default null) + let previous_result = if $previous != null { do $previous $place } 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 +487,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.