smart-snippet and smart-skeleton

随着 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 还是有几个不太方便的地方

  1. 由于是使用单词边界自动触发,因此无法定义非单词的 abbrev ,例如,不能使用 abbrev 让 “(” 自动展开为 “()”。
  2. 以 major-mode 为单位有时候还是显得粒度不够细。例如,我为 c++-mode 定义了一个把 “class” 展开为一个定义 class 的框架,这样很方便,但是我不想在我写注释的时候输入 “class” 却突然展开出一大堆东西。通常的解决办法有两种:
    • 手工不触发 abbrev ,就是在输入 “class” 之后的一个字符,例如,要输入 “SPACE”,现在使用 “C-q SPACE” 来键入。
    • 更改 abbrev ,例如,定义 “classx” ,在需要展开的时候输入 “classx” ,而不影响正常的输入。msf-abbrev.el 就是选择的这种方法

    然而,实际上,两种方法都不是那么舒服,关键就是 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 关系不那么大的内容:在让 " 扩展为 "$." 之后,如何“跳出”引号(也就是把光标移动到引号的后面)呢?我目前所知的有几种解决方案:

  • 直接用方向键,但是使用 Emacs 或者 Vim 的人通常都会觉得方向键太远了,很难按。当然 Emacs 和 Vim 都有专门用于移动光标的快捷键(Emacs 里面可以使用 C-f ,而 Vim 里面虽然直接 l 就可以了,但是却要先按一下 ESC),不过如果是对于非常“懒”的人来说的话,他们仍然是很麻烦的。
  • 一些编辑器会允许你直接输入 " ,它会进行判断,并覆盖掉当前这个引号,并把光标移动到后面,不过这似乎有些丧失了原来自动扩展为一对引号的意义了,到头来还是要自己手工输入后面那个引号。从自动扩展得到的唯一的好处就是不会在输入一个引号的时候,后面整个一片被解释为字符串,显示一片语法高亮,看上去很不爽,让你迫不及待地想赶紧加上另外一个引号,让编辑器能正确地解析语法高亮。
  • 使用一个通用的快捷键来“跳出”。例如 Emacs 的一个扩展 cdlatex 作为一个“让输入变成享受”的典范,就使用的这种方法,它使用一个非常好按的键:Tab 键来完成这个功能。

我在这里也提供一个针对 c++-mode 的“跳出” snippet 的函数。其实 Tab 真的被用的太多了,通常我们写一个包装函数,用于在不同的环境下让 Tab 来完成不同的工作,但是 Emacs 通常有各种扩展,为了避免各个扩展之间的兼容性,对于这种“核心”的功能键,一般还是不要改为妙。幸运的是,Emacs 在 X 模式下(就是说,不是通过 -nw 选项来运行的终端模式)有“两个” Tab 可以用:

  • C-i 也就是通常认为的 Tab ,在终端下是不区分 TabC-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)

其实编辑器的设计也是一门学问呢!让程序员能够更舒服地写代码,无疑是非常重要的话题。

你可能感兴趣的:(C++,c,C#,vim,emacs)