diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a16fd5..41d949e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,20 @@ CHANGELOG - Bug fixes and improvements - Vim plugin: Floating windows support - bash: Various improvements in key bindings (CTRL-T, CTRL-R, ALT-C) +- Fuzzy completion API changed + ```sh + # Previous: fzf arguments given as a single string argument + # - This style is still supported, but it is deprecated + _fzf_complete "--multi --reverse --prompt=\"doge> \"" "$@" < <( + echo foo + ) + + # New API: multiple fzf arguments before "--" + # - More rebust and easier to write options + _fzf_complete --multi --reverse --prompt="doge> " -- "$@" < <( + echo foo + ) + ``` 0.20.0 ------ diff --git a/README.md b/README.md index 669881b..bfd8580 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Table of Contents * [Environment variables / Aliases](#environment-variables--aliases) * [Settings](#settings) * [Supported commands](#supported-commands) + * [Custom fuzzy completion](#custom-fuzzy-completion) * [Vim plugin](#vim-plugin) * [Advanced topics](#advanced-topics) * [Performance](#performance) @@ -414,7 +415,7 @@ _fzf_comprun() { case "$command" in cd) fzf "$@" --preview 'tree -C {} | head -200' ;; - export|unset) fzf "$@" --preview "eval 'echo \$'{}" "$@" ;; + export|unset) fzf "$@" --preview "eval 'echo \$'{}" ;; ssh) fzf "$@" --preview 'dig {}' ;; *) fzf "$@" ;; esac @@ -433,6 +434,56 @@ _fzf_setup_completion path ag git kubectl _fzf_setup_completion dir tree ``` +#### Custom fuzzy completion + +_**(Custom completion API is experimental and subject to change)**_ + +For a command named _"COMMAND"_, define `_fzf_complete_COMMAND` function using +`_fzf_complete` helper. + +```sh +# Custom fuzzy completion for "doge" command +# e.g. doge ** +_fzf_complete_doge() { + _fzf_complete --multi --reverse --prompt="doge> " -- "$@" < <( + echo very + echo wow + echo such + echo doge + ) +} +``` + +- The arguments before `--` are the options to fzf. +- After `--`, simply pass the original completion arguments unchanged (`"$@"`). +- Then write a set of commands that generates the completion candidates and + feed its output to the function using process substitution (`< <(...)`). + +zsh will automatically pick up the function using the naming convention but in +bash you have to manually associate the function with the command using +`complete` command. + +```sh +[ -n "$BASH" ] && complete -F _fzf_complete_doge -o default -o bashdefault doge +``` + +If you need to post-process the output from fzf, define +`_fzf_complete_COMMAND_post` as follows. + +```sh +_fzf_complete_foo() { + _fzf_complete --multi --reverse --header-lines=3 -- "$@" < <( + ls -al + ) +} + +_fzf_complete_foo_post() { + awk '{print $NF}' +} + +[ -n "$BASH" ] && complete -F _fzf_complete_foo -o default -o bashdefault foo +``` + Vim plugin ---------- diff --git a/shell/completion.bash b/shell/completion.bash index bd94c51..b1f7ac9 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -190,6 +190,27 @@ __fzf_generic_path_completion() { } _fzf_complete() { + # Split arguments around -- + local args rest str_arg i sep + args=("$@") + sep= + for i in "${!args[@]}"; do + if [[ "${args[$i]}" = -- ]]; then + sep=$i + break + fi + done + if [[ -n "$sep" ]]; then + str_arg= + rest=("${args[@]:$((sep + 1)):${#args[@]}}") + args=("${args[@]:0:$sep}") + else + str_arg=$1 + args=() + shift + rest=("$@") + fi + local cur selected trigger cmd post post="$(caller 0 | awk '{print $2}')_post" type -t "$post" > /dev/null 2>&1 || post=cat @@ -200,7 +221,7 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $1" __fzf_comprun "$2" -q "$cur" | $post | tr '\n' ' ') + selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" if [ -n "$selected" ]; then COMPREPLY=("$selected") @@ -210,8 +231,7 @@ _fzf_complete() { printf '\e[5n' return 0 else - shift - _fzf_handle_dynamic_completion "$cmd" "$@" + _fzf_handle_dynamic_completion "$cmd" "${rest[@]}" fi } @@ -243,7 +263,7 @@ _fzf_complete_kill() { } _fzf_host_completion() { - _fzf_complete '+m' "$@" < <( + _fzf_complete +m -- "$@" < <( cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | @@ -252,13 +272,13 @@ _fzf_host_completion() { } _fzf_var_completion() { - _fzf_complete '-m' "$@" < <( + _fzf_complete -m -- "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_alias_completion() { - _fzf_complete '-m' "$@" < <( + _fzf_complete -m -- "$@" < <( alias | sed 's/=.*//' | sed 's/.* //' ) } diff --git a/shell/completion.zsh b/shell/completion.zsh index 95f6d68..e82de4c 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -107,16 +107,38 @@ _fzf_feed_fifo() ( ) _fzf_complete() { - local fifo fzf_opts lbuf cmd matches post + setopt localoptions ksh_arrays + # Split arguments around -- + local args rest str_arg i sep + args=("$@") + sep= + for i in {0..$#args}; do + if [[ "${args[$i]}" = -- ]]; then + sep=$i + break + fi + done + if [[ -n "$sep" ]]; then + str_arg= + rest=("${args[@]:$((sep + 1)):${#args[@]}}") + args=("${args[@]:0:$sep}") + else + str_arg=$1 + args=() + shift + rest=("$@") + fi + + local fifo lbuf cmd matches post fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" - fzf_opts=$1 - lbuf=$2 + lbuf=${rest[0]} cmd=$(__fzf_extract_command "$lbuf") - post="${funcstack[2]}_post" + post="${funcstack[1]}_post" + echo "$post" type $post > /dev/null 2>&1 || post=cat _fzf_feed_fifo "$fifo" - matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') + matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi @@ -125,14 +147,14 @@ _fzf_complete() { } _fzf_complete_telnet() { - _fzf_complete '+m' "$@" < <( + _fzf_complete +m -- "$@" < <( command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_ssh() { - _fzf_complete '+m' "$@" < <( + _fzf_complete +m -- "$@" < <( setopt localoptions nonomatch command cat <(cat ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ @@ -142,19 +164,19 @@ _fzf_complete_ssh() { } _fzf_complete_export() { - _fzf_complete '-m' "$@" < <( + _fzf_complete -m -- "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_complete_unset() { - _fzf_complete '-m' "$@" < <( + _fzf_complete -m -- "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' ) } _fzf_complete_unalias() { - _fzf_complete '+m' "$@" < <( + _fzf_complete +m -- "$@" < <( alias | sed 's/=.*//' ) } diff --git a/test/test_go.rb b/test/test_go.rb index fa4a6ef..9bacb8b 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2064,6 +2064,21 @@ module CompletionTest tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } end + + def test_custom_completion_api + %w[f g].each do |command| + tmux.prepare + tmux.send_keys "#{command} b**", :Tab + tmux.until do |lines| + lines.item_count == 2 && lines.match_count == 1 && + lines.any_include?("prompt-#{command}") && + lines.any_include?("preview-#{command}-bar") + end + tmux.send_keys :Enter + tmux.until { |lines| lines[-1].include?("#{command} #{command}barbar") } + tmux.send_keys 'C-u' + end + end end class TestBash < TestBase @@ -2149,3 +2164,41 @@ source "<%= BASE %>/shell/key-bindings.<%= __method__ %>" PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100 unset <%= UNSETS.join(' ') %> + +# Old API +_fzf_complete_f() { + _fzf_complete "--multi --prompt \"prompt-f> \"" "$@" < <( + echo foo + echo bar + ) +} + +# New API +_fzf_complete_g() { + _fzf_complete --multi --prompt "prompt-g> " -- "$@" < <( + echo foo + echo bar + ) +} + +_fzf_complete_f_post() { + awk '{print "f" $0 $0}' +} + +_fzf_complete_g_post() { + awk '{print "g" $0 $0}' +} + +[ -n "$BASH" ] && complete -F _fzf_complete_f -o default -o bashdefault f +[ -n "$BASH" ] && complete -F _fzf_complete_g -o default -o bashdefault g + +_fzf_comprun() { + local command=$1 + shift + + case "$command" in + f) fzf "$@" --preview 'echo preview-f-{}' ;; + g) fzf "$@" --preview 'echo preview-g-{}' ;; + *) fzf "$@" ;; + esac +}