随着 Ruby on Rails 火起来,TextMate 也突然变得很火。没有 Mac 机器,不能体验到 TextMate 是什么感觉,不过在网络上看到 TextMate 的视频演示,其中有一个功能确实是很不错的。就是那个 snippet 。定义一些模板,然后在合适的时候展开,以减少输入重复的内容,这个是每个稍微强一点的编辑器都有的功能了,同一个“域”,在多处展开也是非常常见的功能。但是通常的编辑器都是对每个会放到多个地方的“域”进行提问(例如,弹出一个对话框),TextMate 让人耳目一新的地方就是它并不弹出对话框进行提问,而是直接让你在“域”的地方输入,并在输入的同时同步更新其他几个相关联的“域”的内容。
同步更新与原来的一个一个提问的方法相比,简直就是太 Cool 了!一时间各大编辑器也开始模仿这个功能。Emacs 自然也不落后,很快就有人做出了 snippet.el ,在 Emacs 里面实现了 TextMate 的那种很 Cool 的功能。
snippet.el 适用 Emacs 内建的 abbrev 的功能来实现。Emacs 的 abbrev 与其他一些编辑器不一样,它不需要某一个特定的快捷键来触发(或者说,有许多不同的“快捷键”可以让它展开),当你打开 abbrev-mode 之后,它会在你键入的过程中自动展开,输入一个单词,然后键入空格、标点符号、回车等所有可以作为单词分界的内容时,abbrev 就会被展开。内置的 abbrev 以 major-mode 为单位,可以为不同的 major-mode 定义不同的 abbrev-table 。然而,内建的 abbrev 还是有几个不太方便的地方
C-q
SPACE” 来键入。然而,实际上,两种方法都不是那么舒服,关键就是 Emacs 没有区分在同一个 major-mode 里面的不同语法上下文。
于是我着手做了一个基于 snippet.el 的 smart-snippet.el ,提供更细粒度的控制,正如其名字那样,它更聪明。允许你定义不同上下文展开为不同的模板,或者干脆不展开。非常好用。
我使用 snippet.el 的展开引擎来解析和展开模板,并做了一些小小的更改。 snippet.el 的模板语法非常简单:
$${field}
定义一个域,同名的域在编辑的时候会同步更新。使用 Tab 和 S-Tab 在各个域之间移动。$.
最后光标所处的位置。$>
表示进行一次自动缩进。Emacs 通常对各个 major-mode 都提供非常好的自动缩进功能。 并且这些语法都是可以定制的,如果它们和某一种语言的语法冲突,导致 Emacs 缩进的时候被搞晕了的话,可以更改为其他不会引起混乱的标记,你可以为不同的 major-mode 定义不同的一套模板语法。例如,为 c++-mode
,我可以定义 if
在正常情况下展开为
if ($${cond}) {$> $>$. }$>
而在其他情况,例如字符串或者注释里面就不展开。还有其他一些语言,如 Perl 、Ruby 等,同样的关键字有不同的用法。例如,在 Ruby 里面,if
就有这两种写法
# one way if cond do_something end # another way do_something if cond
smart-snippet.el 的 smart 就在这里能派上用场了!我在项目主页上上传了一个视频演示,展示了 smart-snippetl.el 的功能。
然而,Emacs 内建的 abbrev 的另外一个不足还没有解决。其实这个很好解决,只需要把引号、括号等键绑定到相应的输入一对引号、一对括号的函数上就可以了。Emacser 们通常都使用 Emacs 自带的 skeleton
功能来解决这个问题。然而 smart-snippet.el 在这里仍然能派上用场。我已经扩展了 smart-snippet.el ,让你能够轻松地把一个 snippet 绑定到一个键上。你可以
"
在普通代码里面展开为 "$."
;而如果它本来就在字符串里面,则展开为一个转义的引号 \"
。 <
在正常情况下不展开(作为小于号),而当你已经键入了 template
接下来要写模板参数的时候,展开为 < $. >
。关于具体如何配置以及更多详细的内容,可以参考 smart-snippet.el 的项目主页。
最后,我再说一点和 smart-snippet 关系不那么大的内容:在让 "
扩展为 "$."
之后,如何“跳出”引号(也就是把光标移动到引号的后面)呢?我目前所知的有几种解决方案:
C-f
,而 Vim 里面虽然直接 l
就可以了,但是却要先按一下 ESC
),不过如果是对于非常“懒”的人来说的话,他们仍然是很麻烦的。"
,它会进行判断,并覆盖掉当前这个引号,并把光标移动到后面,不过这似乎有些丧失了原来自动扩展为一对引号的意义了,到头来还是要自己手工输入后面那个引号。从自动扩展得到的唯一的好处就是不会在输入一个引号的时候,后面整个一片被解释为字符串,显示一片语法高亮,看上去很不爽,让你迫不及待地想赶紧加上另外一个引号,让编辑器能正确地解析语法高亮。Tab
键来完成这个功能。我在这里也提供一个针对 c++-mode
的“跳出” snippet 的函数。其实 Tab
真的被用的太多了,通常我们写一个包装函数,用于在不同的环境下让 Tab
来完成不同的工作,但是 Emacs 通常有各种扩展,为了避免各个扩展之间的兼容性,对于这种“核心”的功能键,一般还是不要改为妙。幸运的是,Emacs 在 X 模式下(就是说,不是通过 -nw
选项来运行的终端模式)有“两个” Tab
可以用:
C-i
也就是通常认为的 Tab
,在终端下是不区分 Tab
和 C-i
的。<tab>
这个才是真正的对应到键盘上的那个 Tab
键。于是我们就可以分别用 <tab>
和 C-i
来做不同的事情了。
;; jump out from a pair(like quote, parenthesis, etc.) (defun kid-c-escape-pair () (interactive) (let ((pair-regexp "[^])}"'>]*[])}"'>]")) (if (looking-at pair-regexp) (progn ;; be sure we can use C-u C-@ to jump back ;; if we goto the wrong place (push-mark) (goto-char (match-end 0))) (c-indent-command)))) ;; note TAB can be different to <tab> in X mode(not -nw mode). ;; the formal is C-i while the latter is the real "Tab" key ;; in your keyboard. (define-key c++-mode-map (kbd "TAB") 'kid-c-escape-pair) (define-key c++-mode-map (kbd "<tab>") 'c-indent-command) ;; snippet.el use TAB, now we need to use <tab> (define-key snippet-map (kbd "<tab>") 'snippet-next-field)
其实编辑器的设计也是一门学问呢!让程序员能够更舒服地写代码,无疑是非常重要的话题。