smart-snippet and smart-skeleton

May 21, 2007 – 8:58 pm

随着 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 键。

于是我们就可以分别用 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  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 "") 'c-indent-command)
;; snippet.el use TAB, now we need to use 
(define-key snippet-map  (kbd "") 'snippet-next-field)

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

  1. 22 Responses to “smart-snippet and smart-skeleton”

  2. Great! But I am vimer :(

    By Jack on May 21, 2007

  3. Vim is powerful. There’s no doubt that vim can also archive this, except that the vim script is ugly. :P

    By pluskid on May 21, 2007

  4. 你好,很希望能认识你,我最近在玩emacs, 有些问题,希望能请教你,我在北京
    msn:maode_mao AT hotmail.com 加我

    By mmao on Jun 28, 2007

  5. emm, it seems you should show your contact information so that someone can connect you if you like :p

    By Jack on Jun 29, 2007

  6. to mmaa:

    很高兴认识你!如果想和大家互相交流 Emacs 的话,国内有一个好去处就是 newsmth BBS 的 Emacs 版。 :)

    By pluskid on Jun 29, 2007

  7. I’ve been using this package for some time. I patched it a bit for multiple abbrev selection (like textmate’s selection menu implemented as a menu buffer popup). I wish snippet.el to fully support textmate’s snippet functionality so that I can download and extract all the tab triggers from http://macromates.com/svn/Bundles/trunk/Bundles/ and use sit in emacs without any modifications!

    By Jay on Sep 10, 2007

  8. Hi Jay!
    I also like snippet.el, that’s why I write the smart-snippet.el. Though I don’t have chance to experience Textmate, I’m very glad to hear that you make a patch so that we can use and share the tag triggers without any modifications. But where can I find your patch? Thanks!

    By pluskid on Sep 10, 2007

  9. 为什么我这样配置的

    (require 'smart-snippet)
    (smart-snippet-with-abbrev-tables
        (c++-mode-abbrev-table)
            ("for" "for ($$(a) $$(b) $$(c))" 'bol?)
    )

    但我输入for,然后再输入ddd的时候却是这样: for (addd bddd cddd)

    By ahei on Nov 22, 2007

  10. to ahei:
    请问你修改过了 snippet-field-default-beg-char 这些变量了吗?如果没有改过的话,应该是 “for ($${a} $${b} $${c})” 这样的吧。如果改过了仍然有问题的话,能否把你的关于 smart-snippet 和 c++-mode 的完整配置贴出来我看看可能是哪里的问题?

    ps:多谢你对 smart-snippet.el 的兴趣! :)

    By pluskid on Nov 22, 2007

  11. 这是我的配置

    ;; snippet
    (add-hook 'c++-mode-hook
              (lambda ()
                (setq snippet-field-default-beg-char ?\()
                (setq snippet-field-default-end-char ?\))
                (setq snippet-exit-identifier "$;")))
     
    (require 'smart-snippet)
    (smart-snippet-with-abbrev-tables
     (c++-mode-abbrev-table)
      ("for" "for ($$(a) $$(b) $$(c))" 'bol?)
    )

    我的emacs-version是GNU Emacs 21.3.1 (i386-redhat-linux-gnu, X toolkit, Xaw3d scroll bars) of 2005-02-26 on guru.build.karan.org
    呵呵,我对这个snippet挺感兴趣的,其实我对msf-abbrev更感兴趣,我觉得它那个管理abbrev的方法比较好,snippet的管理感觉不方便,假如我有好多snippet的话放在配置文件里太长了,可惜我的emacs版本是21,msf-abbrev在21上面运行有些小bug.
    请问你有qq或msn吗,我对emacs非常感兴趣,自己也一直在emacs下做开发,有好多emacs方面的问题想请教你呢.我的qq:8261525,msn:ahei080210114@hotmail.com

    By ahei on Nov 23, 2007

  12. to ahei:
    嗯,你运行的时候有出错吗? smart-snippet.el 应该是需要比较高版本的 Emacs 才能正常运行的。现在 Emacs 22 已经正式发布了,如果可以的话,最好还是升级一下,要不然好多新出来的扩展都不好用呢。其实我自己的话,一直都是用 CVS 版的 Emacs 23 ,也蛮稳定的。

    msf-abbrev 的那种方式确实很不错,我也觉得如果结合两者这个特性的话,就能做出 Textmate 的 bundle 那种效果来,最好是能够直接解析 Textmate 的 bundle 格式,或者可以方便转换,这样就可以共享很多东西啦!

    By pluskid on Nov 25, 2007

  13. 我在emacs 21下用snippet都可以的,就是你的smart-snippet不行,郁闷哦

    By ahei on Nov 25, 2007

  14. 你的smart-snippet在emacs 21下运行有一个错误,说looking-back这个函数没有定义,我把emacs 22中的这个函数定义加到.emacs中就没有错误了,但是没有出来应有的效果,msf-abbrev在emacs21下运行也不行,呵呵,不过经过我的修改,在emacs 21下可以运行了,但是你的smart-snippet就不知道怎么改了.我装过emacs 22的,但是我在c++-mode下按tab键的时候它提示说c-narrow-out-enclosing-class没有定义,很奇怪的错误,我在我的.emacs里面找了一下也没找到错误的地方,改天我装emacs cvs 23试试

    By ahei on Nov 25, 2007

  15. to ahei:
    嗯,是的,looking-back 是后面才加进来的函数,我手里面也没有 Emacs 21 ,所以写的时候也没有考虑老版本的了。至于除了 looking-back 之外是不是还有其他问题我也不清楚了。 :(

    By pluskid on Nov 25, 2007

  16. 牛人,什么时候把msf-abbrev也smart一下啊,很期待啊

    By ahei on Nov 27, 2007

  17. 先谢谢pluskid,这里说下本人使用遇到的问题。
    这个smart-snippet似乎与emacs-rails有冲突。
    emacs-rails是用tab按键触发template的,而smart-snippet用space,当在.rb文件下使用tab的触发template的时候,就报错,能smart-snippet的配置去掉就正常了。本人不懂elisp语言,不知道怎么改。

    By mifly on Dec 31, 2007

  18. @mifly:
    你好,你能贴一下你的 smart-snippet 相关的配置吗?

    By pluskid on Dec 31, 2007

  19. 谢谢你你回复。
    配置与在google code上的example一样。
    在没有加入example的代码的时候,emacs-rails是正常使用的,当加入后,ruby-mode使用下每当按下tab键就出错了。
    而在c++-mode下却又正常的。把增加的smart-snippet代码删除,ruby-mode又正常.配置改过,如果确实需要的话,再给你也不迟,只是告诉你,没改smart-snippet的代码。

    By mifly on Jan 1, 2008

  20. @mifly:
    Hi, 我想也许我修正了这个 bug 。原因是 rails-minor-mode 试图 advice snippet-insert 这个函数,然而我这里的 snippet-insert 和它的那个不一样,所以出问题了。我把这个函数改了名字。

    如果我找到的问题和你碰到的问题是同一个的话,你去下载最新的代码应该就可以了。

    ps: 并不是我怀疑你改了 smart-snippet 的代码,只是如果你报告 bug 的话,一般都需要指明详细的步骤和配置来重现这个 bug ,否则我要猜测你的行为,很难甚至根本找不到这个 bug ,就没办法帮你了。 :)

    By pluskid on Jan 1, 2008

  21. 是吗?我也并不是不提供配置,只是配置太长了,而且snippet跟example的一模一样,那个emacs-rails的配置就从http://www.credmp.org/index.php/2006/11/28/ruby-on-rails-and-emacs/
    那里。我现在就更新代码试试。

    By mifly on Jan 1, 2008

  22. 应该是这个问题解决了,谢谢

    By mifly on Jan 1, 2008

  23. @mifly: :)

    By pluskid on Jan 2, 2008

Post a Comment