fzf 进阶使用

定制 fzf 的触发键

除了少数命令,比如 killssh 等,绝大多数命令需要以 ** 为前缀触发 fzf补全。比如:

cd **<Tab>

更多操作可以参考官方文档

但对于一些自定义的高频操作,我希望能直接通过 Tab 键进行补全。比如当我要删除某个分支,此时我并不关心目录内有哪些文件,我只想要按下 Tab 键,并从几个特定的分支中选择一个即可。有人给官方提过 issue,但惨遭拒绝,理由竟然是:“我不太熟悉 zsh,加上这个功能需要大量测试,并且不能保证稳定性”。

好在我们看下源码,结合开原方案,还是有办法实现的。在 zsh 中,默认通过以下方式来使用 fzf:

# 写在 .zshrc 配置中
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

可以先把它拷贝出一份,然后自己修改,我定制过的 fzf 文件如此次 Commit 所示。核心在于这一行:

typeset -A FZF_PER_CMD_COMPLETION_TRIGGERS=(ssh '' vim '*')

这是 zsh 中字典的语法,表示 ssh 命令不需要触发前缀,直接按下 Tab 即可。vim 命令的触发前缀为单个星号。

更方便的删除远程分支

最近结果见我的这次提交,以这个场景介绍实现原理。

我们都知道删除远程分支的方式是:git push origin :$remote_branch,但有没有办法能避免手动输入分支名,而且在输入时预览该分支呢,也就是如下图所示效果:

首先,可以参考 fzf 的 Wiki 给出的解决方案,主要利用了 _fzf_complete_COMMAND 的语法特性。当我们定义一个这样的函数:

# Custom fuzzy completion for "doge" command
#   e.g. doge **<TAB>
_fzf_complete_doge() {
  _fzf_complete "--multi --reverse" "$@" < <(
    echo very
    echo wow
    echo such
    echo doge
  )
}

在输入命令 doge 的时候就可以借助 fzf 来补全了,补全的候选选项则是上面命令中,人工提供的四个单词。

按理说这种写法已经能解决问题,但我在实践过程中发现,_fzf_complete 命令似乎不支持预览。好在它的源码也不长,这里可以看到。核心就是两行指令:

  1. 设置 LBUFFER:这是 zsh 的一个特殊变量,表示你当前这一行里的文字
  2. zle redisplay :修改 LBUFFER 后,调用这个命令刷新 UI,否则修改就不会生效

借助这个思路,我们可以写一个简单版的:

_fzf_complete_gbdr() {
    # temp 用来存储要删除的分支名
    local temp
    temp=$(get_branch_name)
    # $1 是当前的命令,也就是 gdbr。
    # 因此这一行表示把原来的命令 gdbr 和通过 fzf 补全的 branch 选项合在一起,修改当前行的文字
    LBUFFER="$1$temp"
    zle redisplay
}

获取分支名是另一个难点,直接贴我的代码:

git branch --remote |\
awk -F / '{ $1=""; print $0}' OFS="/" | cut -c2- |\
 fzf-down --multi --preview-window right:70% --preview 'git show --color=always origin/{1} | head -'$LINES

第一行就是简单的获取远程分支的列表,得到的每一行格式都是:origin/branch_name,但我们在后面 push 的时候,需要的是不带 origin 的分支名,所以第二行做一下处理。

前文讲过,awk -F 的作用是使用自己指定的分隔符,配合上 $1="" 就可以过滤掉前面的 origin,OFS 表示输出的每一列之间的连接方式,默认是空格,所以这里还是用 / 再连回去。

最后很坑爹的一点是,OFS 会出现在每一列之前,也就是如果原来是 origin/feature/a 会得到输出 /feature/a。所以最后还借助 cut 命令来删掉第一个字符。

最后一行则表示把输出结果交给 fzf 来展示,除了样式上的配置,还可以用 --preview 命令来预览。注意这里要补上 origin/

以上是完整的补全流程,别忘了加上 gbdr 的实现,并把这个命令配置为 Tab 键触发补全。完整的代码是:

_fzf_complete_gbdr() {
    # 把 origin/branch 转换成 branch
    local temp
    temp=$(git branch --remote | awk -F / '{ $1=""; print $0}' OFS="/" | cut -c2- | fzf-down --multi --preview-window right:70% --preview 'git show --color=always origin/{1} | head -'$LINES)
    LBUFFER="$1$temp"
    zle redisplay
}

gbdr() {
    git push origin :$1
}

results matching ""

    No results matching ""