From 426bad68f258d63095352a1e4db1073325c86b7b Mon Sep 17 00:00:00 2001 From: xnm Date: Sat, 13 Sep 2025 14:22:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(fish):=20=F0=9F=8E=A3=20Add=20fzf=20key=20?= =?UTF-8?q?bindings=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add fzf key bindings function file with support for: - File search widget (Ctrl+T) - Command history search (Ctrl+R) (replaced by custom implementation) - Directory navigation (Alt+C) (replaced by custom implementation) Bindings work in both normal and insert modes, providing fuzzy search capabilities for fish shell navigation and command history. --- .../functions/fish_user_key_bindings.fish | 2 + .../fish/functions/fzf_key_bindings.fish | 231 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 home/.config/fish/functions/fzf_key_bindings.fish diff --git a/home/.config/fish/functions/fish_user_key_bindings.fish b/home/.config/fish/functions/fish_user_key_bindings.fish index 4f4b3ce..5636473 100644 --- a/home/.config/fish/functions/fish_user_key_bindings.fish +++ b/home/.config/fish/functions/fish_user_key_bindings.fish @@ -1,4 +1,6 @@ function fish_user_key_bindings + fzf_key_bindings + # Execute this once per mode that emacs bindings should be used in fish_default_key_bindings -M insert diff --git a/home/.config/fish/functions/fzf_key_bindings.fish b/home/.config/fish/functions/fzf_key_bindings.fish new file mode 100644 index 0000000..3138f30 --- /dev/null +++ b/home/.config/fish/functions/fzf_key_bindings.fish @@ -0,0 +1,231 @@ +### key-bindings.fish ### +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.fish +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + +# Key bindings +# ------------ +# The oldest supported fish version is 3.1b1. To maintain compatibility, the +# command substitution syntax $(cmd) should never be used, even behind a version +# check, otherwise the source command will fail on fish versions older than 3.4.0. +function 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 + + function __fzf_defaults + # $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS + # $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS + test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40% + string join ' ' -- \ + "--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \ + (test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \ + $FZF_DEFAULT_OPTS $argv[2..-1] + end + + function __fzfcmd + test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40% + if test -n "$FZF_TMUX_OPTS" + echo "fzf-tmux $FZF_TMUX_OPTS -- " + else if test "$FZF_TMUX" = 1 + echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- " + else + echo fzf + end + end + + function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix' + set -l fzf_query '' + set -l prefix '' + set -l dir '.' + + # Set variables containing the major and minor fish version numbers, using + # a method compatible with all supported fish versions. + set -l -- fish_major (string match -r -- '^\d+' $version) + set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2] + + # fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded. + set -l -- match_regex '(?[\s\S]*?(?=\n?$)$)' + set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S' + if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3 + or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p)) + set -- match_regex "(?$prefix_regex)?$match_regex" + end + + # Set $prefix and expanded $fzf_query with preserved trailing newlines. + if test "$fish_major" -ge 4 + # fish v4.0.0 and newer + string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N) + else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2 + # fish v3.2.0 - v3.7.1 (last v3) + string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N) + eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '') + else + # fish older than v3.2.0 (v3.1b1 - v3.1.2) + set -l -- cl_token (commandline --current-token --tokenize | string collect -N) + set -- prefix (string match -r -- $prefix_regex $cl_token) + set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N) + eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '') + end + + if test -n "$fzf_query" + # Normalize path in $fzf_query, set $dir to the longest existing directory. + if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \) + # fish v3.5.0 and newer + set -- fzf_query (path normalize -- $fzf_query) + set -- dir $fzf_query + while not path is -d $dir + set -- dir (path dirname $dir) + end + else + # fish older than v3.5.0 (v3.1b1 - v3.4.1) + if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2 + # fish v3.2.0 - v3.4.1 + string match -q -r -- '(?^[\s\S]*?(?=\n?$)$)' \ + (string replace -r -a -- '(?<=/)/|(?[\s\S]*)' $fzf_query + else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2 + # fish v3.2.0 - v3.7.1 (last v3) + string match -q -r -- '^/?(?[\s\S]*?(?=\n?$)$)' \ + (string replace -- "$dir" '' $fzf_query | string collect -N) + else + # fish older than v3.2.0 (v3.1b1 - v3.1.2) + set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N) + eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '') + end + end + end + + string escape -n -- "$dir" "$fzf_query" "$prefix" + end + + # Store current token in $dir as root for the 'find' command + function fzf-file-widget -d "List files and folders" + set -l commandline (__fzf_parse_commandline) + set -lx dir $commandline[1] + set -l fzf_query $commandline[2] + set -l prefix $commandline[3] + + set -lx FZF_DEFAULT_OPTS (__fzf_defaults \ + "--reverse --walker=file,dir,follow,hidden --scheme=path" \ + "$FZF_CTRL_T_OPTS --multi --print0") + + set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND" + set -lx FZF_DEFAULT_OPTS_FILE + + set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0) + and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' ' + + commandline -f repaint + end + + function fzf-history-widget -d "Show command history" + set -l -- command_line (commandline) + set -l -- current_line (commandline -L) + set -l -- total_lines (count $command_line) + set -l -- fzf_query (string escape -- $command_line[$current_line]) + + set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \ + '--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \ + '--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \ + "--bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \ + '--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c) + + set -lx FZF_DEFAULT_OPTS_FILE + set -lx FZF_DEFAULT_COMMAND + + if type -q perl + set -a FZF_DEFAULT_OPTS --tac + set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\'' + else + set FZF_DEFAULT_COMMAND \ + 'set -l h (builtin history -z --reverse | string split0);' \ + 'for i in (seq (count $h) -1 1);' \ + 'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \ + end + end + + # Merge history from other sessions before searching + test -z "$fish_private_mode"; and builtin history merge + + if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0) + if test "$total_lines" -eq 1 + commandline -- (string replace -a -- \n\t \n $result) + else + set -l a (math $current_line - 1) + set -l b (math $current_line + 1) + commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result) + commandline -a -- '' $command_line[$b..-1] + end + end + + commandline -f repaint + end + + function fzf-cd-widget -d "Change directory" + set -l commandline (__fzf_parse_commandline) + set -lx dir $commandline[1] + set -l fzf_query $commandline[2] + set -l prefix $commandline[3] + + set -lx FZF_DEFAULT_OPTS (__fzf_defaults \ + "--reverse --walker=dir,follow,hidden --scheme=path" \ + "$FZF_ALT_C_OPTS --no-multi --print0") + + set -lx FZF_DEFAULT_OPTS_FILE + set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND" + + if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0) + cd -- $result + commandline -rt -- $prefix + end + + commandline -f repaint + end + + bind \cr fzf-history-widget + bind -M insert \cr fzf-history-widget + + if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND" + bind \ct fzf-file-widget + bind -M insert \ct fzf-file-widget + end + + if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND" + bind \ec fzf-cd-widget + bind -M insert \ec fzf-cd-widget + end + +end +### end: key-bindings.fish ###