EMACS-DOCUMENT

=============>集思广益

PComplete:根据上下文环境进行补全

我在 /What’s New In Emacs 24/系列文章中(part one, part two) 简单地提到过pcomplete(Eshell中使用的可编程补全库)现在也能直接在 M-x shell 中使用了. 这对喜欢shell mode的人来说可真是个好消息, 因为原来的shell mode并没补全功能(而补全的能力是底层shell本身就提供的).

令人吃惊的是,补全机制在Emacs已经存在很久了,但是它在Emacs中很少用到,这使得大多数人对它知之甚少. 事实上,我估计只有Eshell,ERC,Org Mode和现在Shell Mode中采用到了这个包吧.

Programmable, Context-Sensitive Completion

从Emacs 24开始,你什么都不用做就能使用 pcomplete, 当你打开一个新的shell session时,Emacs会自动做好相应的准备. Emacs自带了很多 pcomplete 函数能够加强Emacs之前那种单纯的文件名补全的能力,使之能提供类似bash/zsh的那种基于上下文的补全. 尤其为 scp, ssh, mount, umountmake 命令提供了很好的补全支持.

下面列出的是那些shell mode支持补全的命令(事实上该列表适用于任何支持pcomplete的mode,包括Eshell.)

  • bzip2

    补全参数,并且只列出经过bzip2压缩后的文件.

  • cd

    补全目录.

  • chgrp

    补全系统中已有的祖名.

  • chown

    补全用户和组,但只有使用 user.group 时才有用.

  • cvs

    补全命令和参数选项,还有cvs entry 以及 module.

  • gdb

    补全目录以及具有执行权限的文件

  • gzip

    补全参数并只列出经过gzip压缩过后的文件.

  • kill

    在 - 后面会列出signal,否则的话会补全所有的进程号.

  • make

    补全参数以及目录中的所有makefile; 在 make -f FILE 后则会补全文件中的rule名称.

  • mount

    补全参数,以及 mount -t TYPE 中的文件系统类型.

  • pushd

    与cd的补全方式一样

  • rm

    补全参数,文件名以及目录.

  • rmdir

    补全目录.

  • rpm

    rpm的补全机制实现的非常精妙. 能够根据上下文环境补全几乎所有的子命令,包括补全package名称.

  • scp

    能够补全命令参数, SSH认证过的主机,并且在命令格式为 scp host:/ 时还能补全远程服务器上的文件路径(借助TRAMP来实现).

  • ssh

    补全命令参数,以及SSH认证过的主机.

  • tar

    补全命令参数,尤其能根据上下文智能补全符合POSIX标准的参数,还能补全文件名.

  • time

    补全目录,以及带有执行权限的命令.

  • umount

    补全命令参数,挂载的目录以及文件系统类型(与mount类似)

  • which

    本应能提供对所有二进制文件的补全,但貌似不能正常工作.

  • xargs

    补全目录以及带有可执行权限的文件.

Custom Completion

一个补全库,既然号称 programmable completion 那它自然就是可编程的了.

要用它来实现简单的参数补全是很简单的,但是除此之外的补全功能实现起来就有点棘手了,因为这个库并没有什么文档说明在里面(虽然乐观主义者会说代码就是最好的文档...).

我接下来会演示一下,如何为 git 添加初步的补全功能

首先我们需要确认一下命令参数的顺序; 对于 git 命令来说,这基本上是固定的: git [options] <command> [<args>]

这里我只关注于常用的那几个命令. 把这些命令放在一个list中:

(defconst pcmpl-git-commands
  '("add" "bisect" "branch" "checkout" "clone"
    "commit" "diff" "fetch" "grep"
    "init" "log" "merge" "mv" "pull" "push" "rebase"
    "reset" "rm" "show" "status" "tag" )
  "List of `git' commands")

pcomplete 的语法规则很灵活(clever): 它会根据一定的命名规则来动态地选择调用哪个elisp函数来进行补全(it will use dynamic dispatch to resolve the elisp function provided it is named a certain way). 一个command的补全函数按 pcomplete/COMMANDpcomplete/MAJOR-MODE/COMMAND 这两种规则进行命名. 只要遵照这个命名规则进行命名,就能够实现补全了.

下一步,我们需要提供一个包含有效子命令的列表 – 在这个例子中,这个列表就是 pcmpl-git-commands 的值, 不过实际上,提供给命令 pcomplete-here 的可以是任意的form(译者注:pcomplete-here内部会运行该form,然后将结果作为补全的依据).

(defun pcomplete/git ()
  "Completion for `git'"
  (pcomplete-here* pcmpl-git-commands))

现在,当你按下tab来补全git的第一个参数时,就会列出所有的子命令了.真不错.

现在让我们扩展一下这个函数,让它也为 addrm 子命令添加补全支持. 我希望当子命令是 addrm 时能补全文件名/文件路径.

借助 pcomplete-match 函数,要实现这个功能出奇地容易. pcomplete-match 函数能够检查特定位置的参数是否匹配某个正则表达式. 你应该留意, 这里是在一个 while 循环中调用 pcomplete-here 的; 这样你就可以一个接一个地补全任意多个文件. 使用 pcomplete-here 的好处在于,它能自动忽略那些之前已经补全过的文件 - 这在为 add 子命令提供补全时非常有用.

(defun pcomplete/git ()
  "Completion for `git'"
  ;; Completion for the command argument.
  (pcomplete-here* pcmpl-git-commands)

  ;; complete files/dirs forever if the command is `add' or `rm'.
  (if (pcomplete-match (regexp-opt '("add" "rm")) 1)
      (while (pcomplete-here (pcomplete-entries)))))

Ok, 到目前为止都还比较容易实现. 现在让我们把它变得更加动态一些,我们来扩展这段代码使之支持 git checkout 命令, 补全出本地可用的分支出来.

要实现这个,我们需要一个辅助函数,这个辅助函数将 shell-command 的输出转换成一个elisp列表. 这个应该很容易实现.

变量 pcmpl-git-ref-list-cmd 中保存的是我们希望Emacs运行的shell命令. 这个命令会返回repo中的所有ref,然后我们再根据ref的子类型进行过滤 (heads, tags, etc.). 函数 pcmpl-git-get-refs 接受一个参数,type,用于作为过滤ref的类型参数.

(defvar pcmpl-git-ref-list-cmd "git for-each-ref refs/ --format='%(refname)'"
  "The `git' command to run to get a list of refs")

(defun pcmpl-git-get-refs (type)
  "Return a list of `git' refs filtered by TYPE"
  (with-temp-buffer
    (insert (shell-command-to-string pcmpl-git-ref-list-cmd))
    (goto-char (point-min))
    (let ((ref-list))
      (while (re-search-forward (concat "^refs/" type "/\\(.+\\)$") nil t)
        (add-to-list 'ref-list (match-string 1)))
      ref-list)))

最后,我们把这些代码全部整合起来. 为了让代码更清晰,我改成用cond语句来保证可读性.

(defconst pcmpl-git-commands
  '("add" "bisect" "branch" "checkout" "clone"
    "commit" "diff" "fetch" "grep"
    "init" "log" "merge" "mv" "pull" "push" "rebase"
    "reset" "rm" "show" "status" "tag" )
  "List of `git' commands")

(defvar pcmpl-git-ref-list-cmd "git for-each-ref refs/ --format='%(refname)'"
  "The `git' command to run to get a list of refs")

(defun pcmpl-git-get-refs (type)
  "Return a list of `git' refs filtered by TYPE"
  (with-temp-buffer
    (insert (shell-command-to-string pcmpl-git-ref-list-cmd))
    (goto-char (point-min))
    (let ((ref-list))
      (while (re-search-forward (concat "^refs/" type "/\\(.+\\)$") nil t)
        (add-to-list 'ref-list (match-string 1)))
      ref-list)))

(defun pcomplete/git ()
  "Completion for `git'"
  ;; Completion for the command argument.
  (pcomplete-here* pcmpl-git-commands)  
  ;; complete files/dirs forever if the command is `add' or `rm'
  (cond
   ((pcomplete-match (regexp-opt '("add" "rm")) 1)
    (while (pcomplete-here (pcomplete-entries))))
   ;; provide branch completion for the command `checkout'.
   ((pcomplete-match "checkout" 1)
    (pcomplete-here* (pcmpl-git-get-refs "heads")))))

这就完成了. 为git命令而创建的简单补全机制. 把这些代码放在你的 .emacs 或其他初始化文件中就能用了.