Skip to content

Conversation

@lalvarezt
Copy link

@lalvarezt lalvarezt commented Nov 21, 2025

Adds fuzzy completion for fish shell, triggered by **<TAB>, matching the functionality of bash/zsh completions.

PS: I didn't test the tmux part since I don't use it, but I tried to keep it faithful to the original code

Configuration Examples

# Custom command lists
set -gx FZF_COMPLETION_DIR_COMMANDS "cd pushd rmdir yazi"
set -gx FZF_COMPLETION_FILE_COMMANDS "bat cat head less tail"

# Custom walkers (e.g., include hidden entries for all)
set -gx FZF_COMPLETION_DIR_WALKER "dir,follow,hidden"
set -gx FZF_COMPLETION_FILE_WALKER "file,follow,hidden"
set -gx FZF_COMPLETION_PATH_WALKER "file,dir,follow,hidden"

# Custom options with exclusions
set -l skip ".git,.cache,node_modules,target"
set -gx FZF_COMPLETION_PATH_OPTS "--walker-skip '$skip'"
set -gx FZF_COMPLETION_DIR_OPTS "--walker-skip '$skip'"
set -gx FZF_COMPLETION_FILE_OPTS "--walker-skip '$skip'"

Usage

vim **<TAB>      # Complete files and directories
cd **<TAB>       # Complete directories only
cat **<TAB>      # Complete files only
kill **<TAB>     # Complete process IDs

@bitraid
Copy link
Contributor

bitraid commented Nov 22, 2025

I had a quick look at the script, and I have the following observations:

  • Having ** as the default trigger, makes it no longer possible to insert every file of every directory in the command line, by simply typing **<TAB>.
  • Fails under tmux, unless $FZF_TMUX is set to 0.
  • The main function is called fzf_completion_setup but the name of the file is completion.fish, which means that if users want to place the file in ~/.config/fish/functions, it won't work (the function won't be available).
  • Option prefix (--option=**) is not recognized and is treated as query.
  • Hostname completion does not contain user@.
  • File/dir names containing newlines are not handled correctly.
  • The whole script could be much simpler. There is no need to manually set what to complete for each command, parse host files, get list of function/variable names, run ps, etc. Everything could be done by simply calling the complete builtin. See for example https://github.com/junegunn/fzf/wiki/Examples-(fish)#completion.

Finally, I want to point out that fish v4.x already does fuzzy tab completion, while showing the results in multiple columns (fitting more items in terminal). So, I’m not sure if it’s worth providing and maintaining such script (but of course, that's not for me to decide).

@lalvarezt
Copy link
Author

Hi, thanks for taking the time and the detailed reply

* Having `**` as the default trigger, makes it no longer possible to insert every file of every directory in the command line, by simply typing `**<TAB>`.

True, I was just following the same upstream convention. Granted we could put another symbol like "##" that has no special use in fish (other than being a comment)

* Fails under tmux, unless `$FZF_TMUX` is set to `0`.

Sorry about that, I did mention that I didn't try it under tmux.

* The main function is called `fzf_completion_setup` but the name of the file is `completion.fish`, which means that if users want to place the file in `~/.config/fish/functions`, it won't work (the function won't be available).

This is intentional, the file is meant to be sourced not auto-loaded. Doing fzf --fish | source handles this natively, or you can just source completion.fish in your config.fish. But in the latter then perhaps completion should not be loaded by default 🤔

* Option prefix (`--option=**`) is not recognized and is treated as query.
* File/dir names containing newlines are not handled correctly.

I totally missed that one 👍

* Hostname completion does not contain `user@`.
* The whole script could be much simpler. There is no need to manually set what to complete for each command, parse host files, get list of function/variable names, run `ps`, etc. Everything could be done by simply calling the `complete` builtin. See for example https://github.com/junegunn/fzf/wiki/Examples-(fish)#completion.

Here we might as well drop the ssh, telnet, set, etc. functions altogether, it's true the native fish does it better

Finally, I want to point out that fish v4.x already does fuzzy tab completion, while showing the results in multiple columns (fitting more items in terminal). So, I’m not sure if it’s worth providing and maintaining such script (but of course, that's not for me to decide).

Here I don't fully agree, native fish is not recursive so having fzf does have merit IMO

@bitraid would you mind having a second look 🙏

@bitraid
Copy link
Contributor

bitraid commented Nov 22, 2025

would you mind having a second look

OK, I had another look: Now it fails under tmux when $FZF_TMUX=1, and newlines are still not properly handled.

Here we might as well drop the ssh, telnet, set, etc. functions altogether, it's true the native fish does it better

You could still have the completions, but instead of doing all that manual work, the list could be retrieved by calling complete.

Here I don't fully agree, native fish is not recursive so having fzf does have merit IMO

This is already provided by CTRL-T. And if all other command completions are removed and only file/dir completions are left, I don’t see what more it provides. Also CTRL-T is more robust. Compare for example:

  • ls "foo bar"<CTRL-T> or ls 'foo<CTRL-T> vs ls "foo bar"**<TAB> or ls 'foo**<TAB>
  • ls foo\ bar<CTRL-T> vs ls foo\ bar**<TAB>
  • ls .fish$<CTRL-T> vs ls .fish$**<TAB>
  • ls foo=bar<CTRL-T> vs ls foo=bar**<TAB>
  • ls -- --foo=bar<CTRL-T> vs ls -- --foo=bar**<TAB>

@lalvarezt
Copy link
Author

lalvarezt commented Nov 23, 2025

This is already provided by CTRL-T. And if all other command completions are removed and only file/dir completions are left, I don’t see what more it provides. Also CTRL-T is more robust. Compare for example:

* `ls "foo bar"<CTRL-T>` or `ls 'foo<CTRL-T>` vs `ls "foo bar"**<TAB>` or `ls 'foo**<TAB>`

* `ls foo\ bar<CTRL-T>` vs `ls foo\ bar**<TAB>`

* `ls .fish$<CTRL-T>` vs `ls .fish$**<TAB>`

* `ls foo=bar<CTRL-T>` vs `ls foo=bar**<TAB>`

* `ls -- --foo=bar<CTRL-T>` vs `ls -- --foo=bar**<TAB>`

This new version should work much better, I was reinventing the wheel in some cases.

thoughts?

EDIT: I removed a weird behavior that was only on my system

@bitraid
Copy link
Contributor

bitraid commented Nov 24, 2025

thoughts?

  • Selecting in fzf file/dir names containing newlines is still not addressed.
  • Tokens that contain escaped witespace characters (\n, \r, etc) are not preserved in query.
  • Tokens ending with escaped dollar sign (\$) crash the script, having it anywhere else gives no query.
  • Tokens that contain escaped special characters (\", \', \[, \() crash the script.
  • Option prefix style of -oArg is not handled.
  • Does not take into account the version of fish: it uses non-existing command parameters on old versions (commandline -p), and deprecated parameters on newer versions (commandline -o).

@lalvarezt
Copy link
Author

thanks again for all the time spent in this, all your test cases pass now (unless I missed something)

@junegunn
Copy link
Owner

Thanks for your interest in the project.

My knowledge and experience with fish are quite limited, and @bitraid has effectively been maintaining the fish module. I respect their judgement, so I'd like to go with their call on this.

As the project maintainer, I hope the fish completion aligns with the existing ones, particularly in terms of configuration. You may want to update the README and extend the existing integration tests to cover fish.

@lalvarezt lalvarezt marked this pull request as draft November 25, 2025 08:37
@lalvarezt
Copy link
Author

Following up on your feedback @bitraid @junegunn:

  • Added test/lib/common.fish for fish-specific test helpers
  • Updated test/lib/common.rb to support fish in the test suite
  • Made existing tests shell-agnostic with a trigger() method
  • Added TestFish class
  • Changed string escape to string escape -n to match bash/zsh behavior (backslash escaping instead of quoting)

Added tests for:

  • Option prefix: --option=/path** and -o/path**
  • Filenames with newlines
  • Paths with special chars like [brackets], $dollar
  • Files starting with -- after the -- separator
  • Query patterns ending with $

All fish tests pass (make itest). Some of the new tests I had to mark as fish-only because bash/zsh fail:

  • Option prefix completion (--option=/path, -o/path) - bash/zsh parse the boundaries wrong in some cases
  • Filenames with newlines - bash/zsh replace them with spaces
  • Short option prefix style (-oARG) - not parsed correctly

So it looks like bash/zsh have some issues with these edge cases that fish handles just fine. Can you please verify this on your end?

Copy link
Contributor

@bitraid bitraid left a comment

Choose a reason for hiding this comment

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

Thank you @junegunn for your trust, and @lalvarezt for all your work. The script now seems to have no issues. I also tested it on fish v3.1b1, and there aren't any compatibility issues either. I left some suggestions in code review.

Below is the script with the suggested changes, along with some other modifications/additions. I added a function that uses fzf for commands like ssh/set/functions etc. Currently it doesn't perform multi-selection, but it can be optimized to do so when appropriate, if it is decided to be kept.

script
#     ____      ____
#    / __/___  / __/
#   / /_/_  / / /_
#  / __/ / /_/ __/
# /_/   /___/_/ completion.fish
#
# - $FZF_COMPLETION_TRIGGER         (default: '**')
# - $FZF_COMPLETION_OPTS            (default: empty)
# - $FZF_COMPLETION_PATH_OPTS       (default: empty)
# - $FZF_COMPLETION_DIR_OPTS        (default: empty)
# - $FZF_COMPLETION_FILE_OPTS       (default: empty)
# - $FZF_COMPLETION_DIR_COMMANDS    (default: cd pushd rmdir)
# - $FZF_COMPLETION_FILE_COMMANDS   (default: cat head tail less more nano)
# - $FZF_COMPLETION_NATIVE_COMMANDS (default: ssh telnet set functions type)

function fzf_completion_setup
    # Load helper functions
    fzf_key_bindings

    # Use complete builtin for specific commands
    function __fzf_complete_native
        # Have the command run in a subshell
        set -lx -- FZF_DEFAULT_COMMAND "complete -C\"$argv[1] \" "

        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '--reverse --nth=1 --color=fg:dim,nth:regular' \
            $FZF_COMPLETION_OPTS $argv[2..-1] '--accept-nth=1 --with-shell='(status fish-path)\\ -c)

        set -l result (eval (__fzfcmd))
        and commandline -rt $result

        commandline -f repaint
    end

    # Generic path completion
    function __fzf_generic_path_completion
        set -lx dir $argv[1]
        set -l fzf_query $argv[2]
        set -l opt_prefix $argv[3]
        set -l compgen $argv[4]
        set -l tail " "

        # Set fzf options
        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --scheme=path" "$FZF_COMPLETION_OPTS --print0")
        set -lx FZF_DEFAULT_COMMAND
        set -lx FZF_DEFAULT_OPTS_FILE

        if string match -q -- '*dir*' $compgen
            set tail
            set -a -- FZF_DEFAULT_OPTS "--walker=dir,follow $FZF_COMPLETION_DIR_OPTS"
        else if string match -q -- '*file*' $compgen
            set -a -- FZF_DEFAULT_OPTS "-m --walker=file,follow,hidden $FZF_COMPLETION_FILE_OPTS"
        else
            set -a -- FZF_DEFAULT_OPTS "-m --walker=file,dir,follow,hidden $FZF_COMPLETION_PATH_OPTS"
        end

        # Run fzf
        if type -q "$compgen"
            set -l result (eval $compgen $dir | eval (__fzfcmd) --query=$fzf_query | string split0)
            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -- $result))$tail
        else
            set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -- $result))$tail
        end

        commandline -f repaint
    end

    # Kill completion (process selection)
    function _fzf_complete_kill
        set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse" "$FZF_COMPLETION_OPTS")
        set -lx FZF_DEFAULT_OPTS_FILE

        set -l result (begin
            command ps -eo user,pid,ppid,start,time,command 2>/dev/null
            or command ps -eo user,pid,ppid,time,args 2>/dev/null # BusyBox
            or command ps --everyone --full --windows 2>/dev/null # Cygwin
        end | eval (__fzfcmd) --accept-nth=2 -m --header-lines=1 --no-preview --wrap --query=$argv[1])
        and commandline -rt -- (string join ' ' -- $result)" "
        commandline -f repaint
    end

    # Main completion function
    function fzf-completion
        set -q FZF_COMPLETION_TRIGGER
        or set -l FZF_COMPLETION_TRIGGER '**'

        # Get tokens - use version-appropriate flags
        set -l tokens
        if test (string match -r -- '^\d+' $version) -ge 4
            set tokens (commandline -xpc)
        else
            set tokens (commandline -opc)
        end

        if test -z "$tokens"; or string match -qv -- "*"(string escape -n -- $FZF_COMPLETION_TRIGGER) (commandline -t)
            commandline -f complete
            return
        end

        # Get the command name
        set -l cmd_name $tokens[1]

        # Strip trigger from commandline before parsing
        test -n "$FZF_COMPLETION_TRIGGER"
        and commandline -rt -- (string sub -e -(string length -- "$FZF_COMPLETION_TRIGGER") -- (commandline -t))

        # Parse commandline (now without trigger)
        set -l parsed (__fzf_parse_commandline)
        set -l dir $parsed[1]
        set -l fzf_query $parsed[2]
        set -l opt_prefix $parsed[3]

        # Directory commands
        set -q FZF_COMPLETION_DIR_COMMANDS
        or set -l FZF_COMPLETION_DIR_COMMANDS cd pushd rmdir

        # File-only commands
        set -q FZF_COMPLETION_FILE_COMMANDS
        or set -l FZF_COMPLETION_FILE_COMMANDS cat head tail less more

        # Native completion commands
        set -q FZF_COMPLETION_NATIVE_COMMANDS
        or set -l FZF_COMPLETION_NATIVE_COMMANDS ssh telnet set functions type

        # Route to appropriate completion function
        if functions -q _fzf_complete_$cmd_name
            _fzf_complete_$cmd_name "$fzf_query" "$cmd_name"
        else if contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS
            set -l -- fzf_opt --query=(commandline -t | string escape)
            __fzf_complete_native "$cmd_name" $fzf_opt
        else if contains -- "$cmd_name" $FZF_COMPLETION_DIR_COMMANDS
            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_dir
        else if contains -- "$cmd_name" $FZF_COMPLETION_FILE_COMMANDS
            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_file
        else
            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_path
        end
    end

    # Bind tab to fzf-completion
    bind \t fzf-completion
    bind -M insert \t fzf-completion
end

# Run setup
fzf_completion_setup
diff
diff --git a/completion.fish b/completion.fish
index 5162b6d..f209e82 100644
--- a/completion.fish
+++ b/completion.fish
@@ -4,41 +4,31 @@
 #  / __/ / /_/ __/
 # /_/   /___/_/ completion.fish
 #
-# - $FZF_COMPLETION_TRIGGER         (default: '++')
+# - $FZF_COMPLETION_TRIGGER         (default: '**')
 # - $FZF_COMPLETION_OPTS            (default: empty)
 # - $FZF_COMPLETION_PATH_OPTS       (default: empty)
 # - $FZF_COMPLETION_DIR_OPTS        (default: empty)
 # - $FZF_COMPLETION_FILE_OPTS       (default: empty)
 # - $FZF_COMPLETION_DIR_COMMANDS    (default: cd pushd rmdir)
 # - $FZF_COMPLETION_FILE_COMMANDS   (default: cat head tail less more nano)
-# - $FZF_COMPLETION_NATIVE_COMMANDS (default: ssh telnet set functions)
-# - $FZF_COMPLETION_PATH_WALKER     (default: 'file,dir,follow,hidden')
-# - $FZF_COMPLETION_DIR_WALKER      (default: 'dir,follow')
-# - $FZF_COMPLETION_FILE_WALKER     (default: 'file,follow,hidden')
-# - $FZF_COMPLETION_NATIVE_MODE     (default: 'complete', or 'complete-and-search')
+# - $FZF_COMPLETION_NATIVE_COMMANDS (default: ssh telnet set functions type)
 
 function fzf_completion_setup
     # Load helper functions
     fzf_key_bindings
 
-    # Check fish version
-    set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
-    if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
-        echo "This script requires fish version 3.1b1 or newer." >&2
-        return 1
-    else if not type -q fzf
-        echo "fzf was not found in path." >&2
-        return 1
-    end
-
-    # Delegate to native fish completion for specific commands
-    # Use FZF_COMPLETION_NATIVE_MODE to choose: 'complete' (default) or 'complete-and-search'
+    # Use complete builtin for specific commands
     function __fzf_complete_native
-        if test "$FZF_COMPLETION_NATIVE_MODE" = complete-and-search
-            commandline -f complete-and-search
-        else
-            commandline -f complete
-        end
+        # Have the command run in a subshell
+        set -lx -- FZF_DEFAULT_COMMAND "complete -C\"$argv[1] \" "
+
+        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '--reverse --nth=1 --color=fg:dim,nth:regular' \
+            $FZF_COMPLETION_OPTS $argv[2..-1] '--accept-nth=1 --with-shell='(status fish-path)\\ -c)
+
+        set -l result (eval (__fzfcmd))
+        and commandline -rt $result
+
+        commandline -f repaint
     end
 
     # Generic path completion
@@ -47,54 +37,34 @@ function fzf_completion_setup
         set -l fzf_query $argv[2]
         set -l opt_prefix $argv[3]
         set -l compgen $argv[4]
-        set -l fzf_opts $argv[5]
-        set -l tail $argv[6]
-
-        # Determine walker based on compgen type
-        set -l walker
-        set -l rest
-        if string match -q '*dir*' -- "$compgen"
-            set walker (test -n "$FZF_COMPLETION_DIR_WALKER"; and echo $FZF_COMPLETION_DIR_WALKER; or echo "dir,follow")
-            set rest $FZF_COMPLETION_DIR_OPTS
-        else if string match -q '*file*' -- "$compgen"
-            set walker (test -n "$FZF_COMPLETION_FILE_WALKER"; and echo $FZF_COMPLETION_FILE_WALKER; or echo "file,follow,hidden")
-            set rest $FZF_COMPLETION_FILE_OPTS
-        else
-            set walker (test -n "$FZF_COMPLETION_PATH_WALKER"; and echo $FZF_COMPLETION_PATH_WALKER; or echo "file,dir,follow,hidden")
-            set rest $FZF_COMPLETION_PATH_OPTS
-        end
+        set -l tail " "
 
         # Set fzf options
-        set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
-            "--reverse --walker=$walker --scheme=path" \
-            "$FZF_COMPLETION_OPTS $fzf_opts --print0 $rest")
+        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --scheme=path" "$FZF_COMPLETION_OPTS --print0")
         set -lx FZF_DEFAULT_COMMAND
         set -lx FZF_DEFAULT_OPTS_FILE
 
+        if string match -q -- '*dir*' $compgen
+            set tail
+            set -a -- FZF_DEFAULT_OPTS "--walker=dir,follow $FZF_COMPLETION_DIR_OPTS"
+        else if string match -q -- '*file*' $compgen
+            set -a -- FZF_DEFAULT_OPTS "-m --walker=file,follow,hidden $FZF_COMPLETION_FILE_OPTS"
+        else
+            set -a -- FZF_DEFAULT_OPTS "-m --walker=file,dir,follow,hidden $FZF_COMPLETION_PATH_OPTS"
+        end
+
         # Run fzf
         if type -q "$compgen"
             set -l result (eval $compgen $dir | eval (__fzfcmd) --query=$fzf_query | string split0)
-            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -n -- $result))$tail
+            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -- $result))$tail
         else
             set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
-            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -n -- $result))$tail
+            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -- $result))$tail
         end
 
         commandline -f repaint
     end
 
-    function _fzf_path_completion
-        __fzf_generic_path_completion $argv[1] $argv[2] $argv[3] _fzf_compgen_path -m " "
-    end
-
-    function _fzf_dir_completion
-        __fzf_generic_path_completion $argv[1] $argv[2] $argv[3] _fzf_compgen_dir "" ""
-    end
-
-    function _fzf_file_completion
-        __fzf_generic_path_completion $argv[1] $argv[2] $argv[3] _fzf_compgen_file -m " "
-    end
-
     # Kill completion (process selection)
     function _fzf_complete_kill
         set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse" "$FZF_COMPLETION_OPTS")
@@ -104,65 +74,35 @@ function fzf_completion_setup
             command ps -eo user,pid,ppid,start,time,command 2>/dev/null
             or command ps -eo user,pid,ppid,time,args 2>/dev/null # BusyBox
             or command ps --everyone --full --windows 2>/dev/null # Cygwin
-        end | eval (__fzfcmd) -m --header-lines=1 --no-preview --wrap --print0 --query=$argv[1] | string split0)
-
-        if test (count $result) -gt 0
-            set -l pids
-            for line in $result
-                test -z "$line"; and continue
-                set -l fields (string split -n ' ' -- "$line")
-                if test (count $fields) -ge 2
-                    set -a pids $fields[2]
-                end
-            end
-            if test (count $pids) -gt 0
-                commandline -rt -- (string join ' ' -- $pids)" "
-            end
-        end
+        end | eval (__fzfcmd) --accept-nth=2 -m --header-lines=1 --no-preview --wrap --query=$argv[1])
+        and commandline -rt -- (string join ' ' -- $result)" "
         commandline -f repaint
     end
 
     # Main completion function
     function fzf-completion
-        set -l trigger (test -n "$FZF_COMPLETION_TRIGGER"; and echo "$FZF_COMPLETION_TRIGGER"; or echo '++')
+        set -q FZF_COMPLETION_TRIGGER
+        or set -l FZF_COMPLETION_TRIGGER '**'
 
         # Get tokens - use version-appropriate flags
-        # Fish 4.0+: -x (--tokens-expanded) returns expanded tokens
-        # Fish 3.1-3.7: -o (--tokenize) returns tokenized output (deprecated in 3.2+ but still works)
-        set -l fish_major (string match -r -- '^\d+' $version)
-
         set -l tokens
-        if test "$fish_major" -ge 4
+        if test (string match -r -- '^\d+' $version) -ge 4
             set tokens (commandline -xpc)
         else
             set tokens (commandline -opc)
         end
 
-        if test (count $tokens) -lt 1
-            __fzf_complete_native
-            return
-        end
-
-        # Handle empty trigger with space
-        if test -z "$trigger"; and test (string sub -s -1 -- (commandline -c)) = ' '
-            set -a tokens ""
-        end
-
-        # Check if the trigger is present at the end
-        if test (string sub -s -(string length -- "$trigger") -- (commandline -c)) != "$trigger"
-            __fzf_complete_native
+        if test -z "$tokens"; or string match -qv -- "*"(string escape -n -- $FZF_COMPLETION_TRIGGER) (commandline -t)
+            commandline -f complete
             return
         end
 
-        # Get the command word
-        set -l cmd_word $tokens[1]
+        # Get the command name
+        set -l cmd_name $tokens[1]
 
         # Strip trigger from commandline before parsing
-        set -l raw_token (commandline --current-token 2>/dev/null | string collect)
-        if test -n "$trigger"
-            set -l stripped_token (string replace -r (string escape --style=regex -- "$trigger")'$' '' -- "$raw_token")
-            commandline -rt -- "$stripped_token"
-        end
+        test -n "$FZF_COMPLETION_TRIGGER"
+        and commandline -rt -- (string sub -e -(string length -- "$FZF_COMPLETION_TRIGGER") -- (commandline -t))
 
         # Parse commandline (now without trigger)
         set -l parsed (__fzf_parse_commandline)
@@ -171,28 +111,29 @@ function fzf_completion_setup
         set -l opt_prefix $parsed[3]
 
         # Directory commands
-        set -l d_cmds (string split ' ' -- "$FZF_COMPLETION_DIR_COMMANDS")
-        or set d_cmds cd pushd rmdir
+        set -q FZF_COMPLETION_DIR_COMMANDS
+        or set -l FZF_COMPLETION_DIR_COMMANDS cd pushd rmdir
 
         # File-only commands
-        set -l f_cmds (string split ' ' -- "$FZF_COMPLETION_FILE_COMMANDS")
-        or set f_cmds cat head tail less more
+        set -q FZF_COMPLETION_FILE_COMMANDS
+        or set -l FZF_COMPLETION_FILE_COMMANDS cat head tail less more
 
         # Native completion commands
-        set -l n_cmds (string split ' ' -- "$FZF_COMPLETION_NATIVE_COMMANDS")
-        or set n_cmds ssh telnet set functions
+        set -q FZF_COMPLETION_NATIVE_COMMANDS
+        or set -l FZF_COMPLETION_NATIVE_COMMANDS ssh telnet set functions type
 
         # Route to appropriate completion function
-        if type -q "_fzf_complete_$cmd_word"
-            eval "_fzf_complete_$cmd_word" (string escape -- "$fzf_query") (string escape -- "$cmd_word")
-        else if contains -- "$cmd_word" $n_cmds
-            __fzf_complete_native
-        else if contains -- "$cmd_word" $d_cmds
-            _fzf_dir_completion "$dir" "$fzf_query" "$opt_prefix"
-        else if contains -- "$cmd_word" $f_cmds
-            _fzf_file_completion "$dir" "$fzf_query" "$opt_prefix"
+        if functions -q _fzf_complete_$cmd_name
+            _fzf_complete_$cmd_name "$fzf_query" "$cmd_name"
+        else if contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS
+            set -l -- fzf_opt --query=(commandline -t | string escape)
+            __fzf_complete_native "$cmd_name" $fzf_opt
+        else if contains -- "$cmd_name" $FZF_COMPLETION_DIR_COMMANDS
+            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_dir
+        else if contains -- "$cmd_name" $FZF_COMPLETION_FILE_COMMANDS
+            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_file
         else
-            _fzf_path_completion "$dir" "$fzf_query" "$opt_prefix"
+            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_path
         end
     end

@lalvarezt
Copy link
Author

lalvarezt commented Nov 26, 2025

@bitraid would you mind running make itest on your side, I'm getting some errors with your proposed code. For example

         # Run fzf
         if type -q "$compgen"
             set -l result (eval $compgen $dir | eval (__fzfcmd) --query=$fzf_query | string split0)
-            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -n -- $result))$tail
+            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -- $result))$tail
         else
             set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
-            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -n -- $result))$tail
+            and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -- $result))$tail
         end

here you rollback this change, but before it was behaving like bash/zsh and this way is different. See that it only fails for fish but the test passes for zsh/bash

  5) Failure:
TestFish#test_file_completion [test/test_shell_integration.rb:205]:
Expected: "cat no\\~such\\~user"
  Actual: "cat 'no~such~user'"

EDIT: Another example with this code doing cd **\t shows all files instead of just the dirs

@lalvarezt
Copy link
Author

currently here, but I'm stuck at the moment see #4605 (comment). It's not working like I would expect it to.
I'll take a fresh look at it tomorrow

@lalvarezt
Copy link
Author

@junegunn @bitraid I think this is ready. Can you please review it

@bitraid
Copy link
Contributor

bitraid commented Nov 29, 2025

Probably the README needs some updating, too.

Also, I have a final suggestion: How about changing the order of completion functions, so that __fzf_complete_native be first? That way a user can add kill to $FZF_COMPLETION_NATIVE_COMMANDS_MULTI, so that the completion list of kill is done by the complete built in instead of the ps command:

diff --git a/completion.fish b/completion.fish
index 1d5efb0..fa1fcc4 100644
--- a/completion.fish
+++ b/completion.fish
@@ -154,13 +154,13 @@ function fzf_completion_setup
         or set -l FZF_COMPLETION_NATIVE_COMMANDS_MULTI set functions type
 
         # Route to appropriate completion function
-        if functions -q _fzf_complete_$cmd_name
-            _fzf_complete_$cmd_name "$fzf_query" "$cmd_name"
-        else if contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
+        if contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
             set -l -- fzf_opt --query=(commandline -t | string escape)
             contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
             and set -a -- fzf_opt --multi
             __fzf_complete_native "$cmd_name " $fzf_opt
+        else if functions -q _fzf_complete_$cmd_name
+            _fzf_complete_$cmd_name "$fzf_query" "$cmd_name"
         else if contains -- "$cmd_name" $FZF_COMPLETION_DIR_COMMANDS
             __fzf_generic_path_completion _fzf_compgen_dir
         else if contains -- "$cmd_name" $FZF_COMPLETION_FILE_COMMANDS

@lalvarezt
Copy link
Author

Updated the readme and the changes suggested above

@junegunn
Copy link
Owner

Thank you both for the great work.

Some remaining issues.

Should we allow opting out?

For bash and zsh, the install script lets users opt out of fuzzy completion or key bindings:

Do you want to enable fuzzy auto-completion? ([y]/n) n
Do you want to enable key bindings? ([y]/n) y

The fish version doesn't support this, and it may not be trivial to update the script to properly handle it (because we don't generate ~/.fzf.fish and simply load it, unlike in bash and zsh). I tried something like this:

diff --git a/install b/install
index 6d0149e4..c2b88357 100755
--- a/install
+++ b/install
@@ -362,25 +362,34 @@ for shell in $shells; do
   append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
 done
 
-if [ $key_bindings -eq 1 ] && [[ $shells =~ fish ]]; then
+fish_line=
+if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
+  fish_line='fzf --fish | source'
+elif [[ $key_bindings -eq 1 ]]; then
+  fish_line="fzf --fish | sed -n '/^### key-bindings/,/^### end/p' | source"
+elif [[ $auto_completion -eq 1 ]]; then
+  fish_line="fzf --fish | sed -n '/^### completion/,/^### end/p' | source"
+fi
+
+if [[ -n $fish_line ]] && [[ $shells =~ fish ]]; then
   bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
   if [ ! -e "$bind_file" ]; then
     mkdir -p "${fish_dir}/functions"
     create_file "$bind_file" \
       'function fish_user_key_bindings' \
-      '  fzf --fish | source' \
+      "  $fish_line" \
       'end'
   else
     echo "Check $bind_file:"
     lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
     if [[ -n $lno ]]; then
       echo "  ** Found 'fzf_key_bindings' in line #$lno"
-      echo "  ** You have to replace the line to 'fzf --fish | source'"
+      echo "  ** You have to replace the line to: $fish_line"
       echo
     else
       echo "  - Clear"
       echo
-      append_line $update_config "fzf --fish | source" "$bind_file"
+      append_line $update_config "$fish_line" "$bind_file"
     fi
   fi
 fi

But,

  • it doesn't properly update the config file when you re-run install script with a different set of options.
    ./install --all --no-completion
    cat ~/.config/fish/functions/fish_user_key_bindings.fish
    
    ./install --all --no-key-bindings
    cat ~/.config/fish/functions/fish_user_key_bindings.fish
      # Incorrectly updated
  • and the completion code currently depends on the key bindings.
    rm -f ~/.config/fish/functions/fish_user_key_bindings.fish
    ./install --all --no-key-bindings
    fish
      # fish: Unknown command: fzf_key_bindings

So what do you think? Is it worth supporting opt-out in fish like we do for bash and zsh, or should fish remain an exception?

Custom fuzzy completion

For bash and zsh, we provide a way to write custom fuzzy completion:

_fzf_complete_foo() {
  _fzf_complete --multi --reverse --header-lines=3 -- "$@" < <(
    ls -al
  )
}

# Optional post-processing of selected entries
_fzf_complete_foo_post() {
  awk '{print $NF}'
}

[ -n "$BASH" ] && complete -F _fzf_complete_foo -o default -o bashdefault foo

I see that you implemented the _fzf_complete_SOMETHING convention here, but users still need to know how to precisely implement the function themselves, unlike in bash and zsh where _fzf_complete abstracts those details away.

So what's our stance? Should we try to introduce a similar helper on the fish side to make custom fuzzy completion more approachable? Or is the current level of abstraction sufficient for fish users, and we leave the rest as future work?

@bitraid
Copy link
Contributor

bitraid commented Nov 30, 2025

I think we can address all those issues.

Meanwhile, here are some fixes for some regressions, fixes for command completion when the token contains quotes, a fallback in the off chance that ps is missing, and some remaining optimizations of eval usage:

diff --git a/completion.fish b/completion.fish
index d03f4ee..4b70099 100644
--- a/completion.fish
+++ b/completion.fish
@@ -22,13 +22,13 @@ function fzf_completion_setup
     function __fzf_complete_native
         set -l result
         if type -q column
-            set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse" \
+            set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse \
                 $FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1)
             set result (eval complete -C \"$argv[1]\" \| column -t -s \\t \| (__fzfcmd))
         else
             set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --nth=1 --color=fg:dim,nth:regular" \
                 $FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1)
-            set result (eval complete -C \"$argv[1]\" \| (__fzfcmd))
+            set -- result (eval complete -C \"$argv[1]\" \| (__fzfcmd))
         end
         and commandline -rt -- (string join ' ' -- $result)' '
         commandline -f repaint
@@ -36,20 +36,19 @@ function fzf_completion_setup
 
     # Generic path completion
     function __fzf_generic_path_completion
-        set -l parsed (__fzf_parse_commandline)
-        set -lx dir $parsed[1]
-        set -l fzf_query $parsed[2]
-        set -l opt_prefix $parsed[3]
-        set -l compgen $argv[1]
-        set -l tail " "
+        set -lx -- dir $argv[1]
+        set -l -- fzf_query $argv[2]
+        set -l -- opt_prefix $argv[3]
+        set -l -- compgen $argv[4]
+        set -l -- tail " "
 
         # Set fzf options
-        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --scheme=path" "$FZF_COMPLETION_OPTS --print0")
+        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --scheme=path" $FZF_COMPLETION_OPTS --print0)
         set -lx FZF_DEFAULT_COMMAND
         set -lx FZF_DEFAULT_OPTS_FILE
 
         if string match -q -- '*dir*' $compgen
-            set tail ""
+            set -- tail ""
             set -a -- FZF_DEFAULT_OPTS "--walker=dir,follow $FZF_COMPLETION_DIR_OPTS"
         else if string match -q -- '*file*' $compgen
             set -a -- FZF_DEFAULT_OPTS "-m --walker=file,follow,hidden $FZF_COMPLETION_FILE_OPTS"
@@ -60,9 +59,9 @@ function fzf_completion_setup
         # Run fzf
         set -l result
         if functions -q "$compgen"
-            set result (eval $compgen $dir | eval (__fzfcmd) --query=$fzf_query | string split0)
+            set -- result (eval $compgen $dir \| (__fzfcmd) --query=$fzf_query | string split0)
         else
-            set result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
+            set -- result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
         end
         and commandline -rt -- (string join -- ' ' $opt_prefix(string escape -n -- $result))$tail
 
@@ -71,22 +70,25 @@ function fzf_completion_setup
 
     # Kill completion (process selection)
     function _fzf_complete_kill
-        set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse" "$FZF_COMPLETION_OPTS")
+        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse $FZF_COMPLETION_OPTS \
+            --accept-nth=2 -m --header-lines=1 --no-preview --wrap)
         set -lx FZF_DEFAULT_OPTS_FILE
-
-        set -l result (begin
-            command ps -eo user,pid,ppid,start,time,command 2>/dev/null
-            or command ps -eo user,pid,ppid,time,args 2>/dev/null # BusyBox
-            or command ps --everyone --full --windows 2>/dev/null # Cygwin
-        end | eval (__fzfcmd) --accept-nth=2 -m --header-lines=1 --no-preview --wrap --query=$argv[1])
-        and commandline -rt -- (string join ' ' -- $result)" "
+        if type -q ps
+            set -l -- ps_cmd 'begin command ps -eo user,pid,ppid,start,time,command 2>/dev/null;' \
+                'or command ps -eo user,pid,ppid,time,args 2>/dev/null;' \
+                'or command ps --everyone --full --windows 2>/dev/null; end'
+            set -l -- result (eval $ps_cmd \| (__fzfcmd) --query=$argv[1])
+            and commandline -rt -- (string join ' ' -- $result)" "
+        else
+            __fzf_complete_native "kill " --multi --query=$argv[1]
+        end
         commandline -f repaint
     end
 
     # Main completion function
     function fzf-completion
         set -q FZF_COMPLETION_TRIGGER
-        or set -l FZF_COMPLETION_TRIGGER '**'
+        or set -l -- FZF_COMPLETION_TRIGGER '**'
 
         # Set variables containing the major and minor fish version numbers, using
         # a method compatible with all supported fish versions.
@@ -96,36 +98,42 @@ function fzf_completion_setup
         # Get tokens - use version-appropriate flags
         set -l tokens
         if test $fish_major -ge 4
-            set tokens (commandline -xpc)
+            set -- tokens (commandline -xpc)
         else
-            set tokens (commandline -opc)
+            set -- tokens (commandline -opc)
         end
 
         set -l -- current_token (commandline -t)
         set -l -- cmd_name $tokens[1]
 
+        set -l -- regex_trigger (string escape --style=regex -- $FZF_COMPLETION_TRIGGER)'$'
         set -l -- has_trigger false
-        string match -qr -- (string escape --style regex -- $FZF_COMPLETION_TRIGGER)'$' $current_token
+        string match -qr -- $regex_trigger $current_token
         and set has_trigger true
 
         # Strip trigger from commandline before parsing
         if $has_trigger; and test -n "$FZF_COMPLETION_TRIGGER" -a -n "$current_token"
-            set -- current_token (string sub -e -(string length -- "$FZF_COMPLETION_TRIGGER") -- $current_token)
+            set -- current_token (string replace -r -- $regex_trigger '' $current_token)
             commandline -rt -- $current_token
         end
 
+        set -l -- parsed (__fzf_parse_commandline)
+        set -l -- dir $parsed[1]
+        set -l -- fzf_query $parsed[2]
+        set -l -- opt_prefix $parsed[3]
+
         if not $has_trigger
             commandline -f complete
             return
         else if test -z "$tokens"
-            __fzf_complete_native "" --query=$current_token
+            __fzf_complete_native "" --query=$opt_prefix$fzf_query
             return
         end
 
-        set -l disable_opt_comp false
+        set -l -- disable_opt_comp false
         if not test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
             string match -qe -- ' -- ' (string sub -l (commandline -Cp) -- (commandline -p))
-            and set disable_opt_comp true
+            and set -- disable_opt_comp true
         end
 
         if not $disable_opt_comp
@@ -140,34 +148,34 @@ function fzf_completion_setup
 
         # Directory commands
         set -q FZF_COMPLETION_DIR_COMMANDS
-        or set -l FZF_COMPLETION_DIR_COMMANDS cd pushd rmdir
+        or set -l -- FZF_COMPLETION_DIR_COMMANDS cd pushd rmdir
 
         # File-only commands
         set -q FZF_COMPLETION_FILE_COMMANDS
-        or set -l FZF_COMPLETION_FILE_COMMANDS cat head tail less more nano
+        or set -l -- FZF_COMPLETION_FILE_COMMANDS cat head tail less more nano
 
         # Native completion commands
         set -q FZF_COMPLETION_NATIVE_COMMANDS
-        or set -l FZF_COMPLETION_NATIVE_COMMANDS ssh telnet
+        or set -l -- FZF_COMPLETION_NATIVE_COMMANDS ssh telnet
 
         # Native completion commands (multi-selection)
         set -q FZF_COMPLETION_NATIVE_COMMANDS_MULTI
-        or set -l FZF_COMPLETION_NATIVE_COMMANDS_MULTI set functions type
+        or set -l -- FZF_COMPLETION_NATIVE_COMMANDS_MULTI set functions type
 
         # Route to appropriate completion function
         if contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
-            set -l -- fzf_opt --query=(commandline -t | string escape)
+            set -l -- fzf_opt --query=$fzf_query
             contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
             and set -a -- fzf_opt --multi
             __fzf_complete_native "$cmd_name " $fzf_opt
         else if functions -q _fzf_complete_$cmd_name
             _fzf_complete_$cmd_name "$fzf_query" "$cmd_name"
         else if contains -- "$cmd_name" $FZF_COMPLETION_DIR_COMMANDS
-            __fzf_generic_path_completion _fzf_compgen_dir
+            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_dir
         else if contains -- "$cmd_name" $FZF_COMPLETION_FILE_COMMANDS
-            __fzf_generic_path_completion _fzf_compgen_file
+            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_file
         else
-            __fzf_generic_path_completion _fzf_compgen_path
+            __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_path
         end
     end
 

@lalvarezt
Copy link
Author

makes sense, we might as well do the remaining ones (after your patch)

diff --git a/shell/completion.fish b/shell/completion.fish
index 4b70099f..e5bc3ee0 100644
--- a/shell/completion.fish
+++ b/shell/completion.fish
@@ -24,9 +24,9 @@ function fzf_completion_setup
         if type -q column
             set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse \
                 $FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1)
-            set result (eval complete -C \"$argv[1]\" \| column -t -s \\t \| (__fzfcmd))
+            set -- result (eval complete -C \"$argv[1]\" \| column -t -s \\t \| (__fzfcmd))
         else
-            set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --nth=1 --color=fg:dim,nth:regular" \
+            set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse --nth=1 --color=fg:dim,nth:regular \
                 $FZF_COMPLETION_OPTS $argv[2..-1] --accept-nth=1)
             set -- result (eval complete -C \"$argv[1]\" \| (__fzfcmd))
         end
@@ -43,17 +43,17 @@ function fzf_completion_setup
         set -l -- tail " "
 
         # Set fzf options
-        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --scheme=path" $FZF_COMPLETION_OPTS --print0)
+        set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse --scheme=path $FZF_COMPLETION_OPTS --print0)
         set -lx FZF_DEFAULT_COMMAND
         set -lx FZF_DEFAULT_OPTS_FILE
 
         if string match -q -- '*dir*' $compgen
             set -- tail ""
-            set -a -- FZF_DEFAULT_OPTS "--walker=dir,follow $FZF_COMPLETION_DIR_OPTS"
+            set -a -- FZF_DEFAULT_OPTS --walker=dir,follow $FZF_COMPLETION_DIR_OPTS
         else if string match -q -- '*file*' $compgen
-            set -a -- FZF_DEFAULT_OPTS "-m --walker=file,follow,hidden $FZF_COMPLETION_FILE_OPTS"
+            set -a -- FZF_DEFAULT_OPTS --multi --walker=file,follow,hidden $FZF_COMPLETION_FILE_OPTS
         else
-            set -a -- FZF_DEFAULT_OPTS "-m --walker=file,dir,follow,hidden $FZF_COMPLETION_PATH_OPTS"
+            set -a -- FZF_DEFAULT_OPTS --multi --walker=file,dir,follow,hidden $FZF_COMPLETION_PATH_OPTS
         end
 
         # Run fzf

@bitraid
Copy link
Contributor

bitraid commented Nov 30, 2025

Just keep in mind that when calling __fzf_defaults to set the local FZF_DEFAULT_OPTS, the first argument (enclosed in quotes) is prepended, and the rest are appended to FZF_DEFAULT_OPTS and FZF_DEFAULT_OPTS_FILE.

@lalvarezt
Copy link
Author

Just keep in mind that when calling __fzf_defaults to set the local FZF_DEFAULT_OPTS, the first argument (enclosed in quotes) is prepended, and the rest are appended to FZF_DEFAULT_OPTS and FZF_DEFAULT_OPTS_FILE.

You're right, this changed the order and allows the user to override causing issues

- set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse --nth=1 --color=fg:dim,nth:regular \
+ set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --nth=1 --color=fg:dim,nth:regular" \

- set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults --reverse --scheme=path $FZF_COMPLETION_OPTS --print0)
+ set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --scheme=path" $FZF_COMPLETION_OPTS --print0)

@bitraid
Copy link
Contributor

bitraid commented Nov 30, 2025

Actually, the options in the first argument are the ones that are allowed to be changed by the user variables, while the rest are always in effect.

@lalvarezt
Copy link
Author

what do you think? Is it worth supporting opt-out in fish like we do for bash and zsh, or should fish remain an exception?

We could make this work if we refactor the functions from key-bindings.fish that we use in completion.fish. That way we have 3 files and common.fish, key-bindings.fish, and completion.fish. We can remove the dependence then

On the other hand we could:

  • (works for all shells) add new flags to tweak what's included, like: fzf --fish --no-completion
  • integrate into the current install script like the others (we could generate the file as well and source it)

Custom fuzzy completion

...
I see that you implemented the _fzf_complete_SOMETHING convention here, but users still need to know how to precisely implement the function themselves, unlike in bash and zsh where _fzf_complete abstracts those details away.

So what's our stance? Should we try to introduce a similar helper on the fish side to make custom fuzzy completion more approachable? Or is the current level of abstraction sufficient for fish users, and we leave the rest as future work?

like @bitraid said this shouldn't be a problem. I've started prototyping something, but still needs some work

@bitraid
Copy link
Contributor

bitraid commented Dec 1, 2025

I have also created a solution for this that doesn't need refactoring, it makes use of a variable __fzf_skip_bind that can have the values keys/completion. If it contains keys, fzf_key_bindings only exports its functions. The line in fish_user_key_bindings.fish is set/modified to set -lx __fzf_skipbind keys; fzf --fish | source, set -lx __fzf_skip_bind completion; fzf --fish | source or fzf --fish | source, depending on user choice.

@junegunn
Copy link
Owner

junegunn commented Dec 1, 2025

We could make this work if we refactor the functions from key-bindings.fish that we use in completion.fish. That way we have 3 files and common.fish, key-bindings.fish, and completion.fish.

In bash and zsh, each script file should be individually "source"able for historical reasons.
You can find a very similar discussion here: #4412. In that discussion, we decided to have a common script file, that is injected into both files using a script so that they remain separately usable.

@lalvarezt
Copy link
Author

Should we then merge this one and @bitraid makes another PR on top?

@bitraid
Copy link
Contributor

bitraid commented Dec 1, 2025

Back to the main script, I have yet one more small fix for the following issue: Suppose we trigger for example --opt=val** when completing kill or set, to search if it is part of a process parameters, or if it is contained in the value of some variable. Currently, the query becomes val, with the following change it becomes --opt=val:

diff --git a/completion.fish b/completion.fish
index 17b50aa..5150440 100644
--- a/completion.fish
+++ b/completion.fish
@@ -121,12 +121,13 @@ function fzf_completion_setup
         set -l -- dir $parsed[1]
         set -l -- fzf_query $parsed[2]
         set -l -- opt_prefix $parsed[3]
+        set -l -- full_query $opt_prefix$fzf_query
 
         if not $has_trigger
             commandline -f complete
             return
         else if test -z "$tokens"
-            __fzf_complete_native "" --query=$opt_prefix$fzf_query
+            __fzf_complete_native "" --query=$full_query
             return
         end
 
@@ -164,12 +165,12 @@ function fzf_completion_setup
 
         # Route to appropriate completion function
         if contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
-            set -l -- fzf_opt --query=$fzf_query
+            set -l -- fzf_opt --query=$full_query
             contains -- "$cmd_name" $FZF_COMPLETION_NATIVE_COMMANDS_MULTI
             and set -a -- fzf_opt --multi
             __fzf_complete_native "$cmd_name " $fzf_opt
         else if functions -q _fzf_complete_$cmd_name
-            _fzf_complete_$cmd_name "$fzf_query" "$cmd_name"
+            _fzf_complete_$cmd_name "$full_query" "$cmd_name"
         else if contains -- "$cmd_name" $FZF_COMPLETION_DIR_COMMANDS
             __fzf_generic_path_completion "$dir" "$fzf_query" "$opt_prefix" _fzf_compgen_dir
         else if contains -- "$cmd_name" $FZF_COMPLETION_FILE_COMMANDS

@bitraid
Copy link
Contributor

bitraid commented Dec 1, 2025

Should we then merge this one and @bitraid makes another PR on top?

I think your approach is closer to what is done with bash/zsh (splitting the files). But instead of using different options for fzf, modify the update.sh script to merge the common code, and then the installation can be done similar to the other shells.

@lalvarezt
Copy link
Author

We could make this work if we refactor the functions from key-bindings.fish that we use in completion.fish. That way we have 3 files and common.fish, key-bindings.fish, and completion.fish.

In bash and zsh, each script file should be individually "source"able for historical reasons. You can find a very similar discussion here: #4412. In that discussion, we decided to have a common script file, that is injected into both files using a script so that they remain separately usable.

please have a look now

@junegunn
Copy link
Owner

junegunn commented Dec 2, 2025

The use of 4-space indentation introduces too much unnecessary code changes. Since all script files in this project follow the 2-space indentation convention, I'd like to suggest using 2-space indentation here as well for consistency.

@lalvarezt
Copy link
Author

The use of 4-space indentation introduces too much unnecessary code changes. Since all script files in this project follow the 2-space indentation convention, I'd like to suggest using 2-space indentation here as well for consistency.

Oh, my bad, it was not intentional

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants