EMACS-DOCUMENT

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

让Emacs为你自动插入内容(Emacs模板使用指南)

我们经常要打字. 而对于文本文件来说,很多的输出只是为了组织内容而已. 比如org-mode中的星号,空格以及 #+, 比如编程代码中的那些括号, do..end 之类的.

不过也没谁规定这些内容必须是由你手工输入的呀.

下面介绍几种让Emacs为你自动输入的几种方法… 比如我们可以在新建某类文件时自动插入一份样板文.

注意,在本教材中,我不会将关于自动补全的内容,所以不会讲到 auto-completecompany.

1 Introduction to YAS

yasnippet 可以让你快速插入代码片段. 所谓片段是指一份模板,你可以手工或用程序来替换模板中的某些内容. 至于会选择哪个模板来扩展则要看buffer的mode了.

刚开始的时候, 让我们先配置一下YAS让它插入一段固定的文本吧. 我这里假设你安装了use-package, 让我们先安装yasnippet然后为我们的模板设置一个独立的目录(接下来我这里会混用片段和模板这两种说法):

(use-package yasnippet
  :ensure t
  :init
  (yas-global-mode 1)
  :config
  (add-to-list 'yas-snippet-dirs (locate-user-emacs-file "snippets")))

然后我们可以按下 C-c C-n 来创建一个模板了,如果记不住快捷键的话也没关系,输入 M-x yas-new-snippet 也行(而且如果你开启了类似IDO的插件,这个速度也不慢).

你应该会进入一个新的template buffer中了, 而且由于它还使用了YAS,你可以输入一个域的内容后,按下 Tab 键来跳转到下一个域的位置. 假设你输入的内容是这样的:

# -*- mode: snippet -*-
# name: blah
# key: blah
# --
Bling blargh-a bloo bloop!

按下 C-c C-c 来应用该模板. Emacs会询问你这个模板需要在哪个mode下使用. 基本上你只需要直接按下回车就行了.

如果你想保存下来这个模板,推荐你保存到 ~/.emacs.d/snippets 目录下,这也是默认的保存地址.

现在,在相同mode的buffer中输入 blah 然后按下 Tab, blah 会被扩展为: Bling blargh-a bloo bloop!

YAS还提供了多种触发模板的方法,不过下一步让我们修改模板让它变得更有用一些吧.

2 Interactive Snippets

将一个简短的内容扩充为一长段内容当然很有用,若能让模板扩展的结果能适应上下文环境那就更好了. 首先我们让模板的某些内容变得容易修改起来.

在某个编程语言的mode下打开一份代码文件,然后新建一个模板. 比如我要为JavaScript创建一份ifelse的代码片段:

# -*- mode: snippet -*-
# name: ifelse
# key: ife
# --
if ($1) {
  $0;
}
else {
}

现在我在JavaScript mode下输入 ife 按下 Tab 就能扩展出一个 if.. else 模板了, 而且你会看到光标定位到了括号中间($1指定了光标位置), 我们可以直接输入条件了. 按下 Tab 后会跳转到大括号之间,这样我iu可以直接输入符合条件的执行语句了(因为$0标示了域编辑结束的地方)

关于YAS,我还没讲完了,不过让我们先讲点其他的,这样我的下一个YAS例子才显得有意义.

3 New Files

还记得公司突然要求在每个文件头部加上版权声明的那个时候吗? 我是不太清楚这样做有多大的法律效力啦,不过Emacs很早以前就自带了 Auto Insert 功能了. 它可以为某种特定mode的新文件设置一个样板文件.

下面配置使用 use-package 来为你的新文件配置样板文件:

(use-package autoinsert
  :init
  ;; Don't want to be prompted before insertion:
  (setq auto-insert-query nil)

  (setq auto-insert-directory (locate-user-emacs-file "templates"))
  (add-hook 'find-file-hook 'auto-insert)
  (auto-insert-mode 1)

  :config
  (define-auto-insert "\\.html?$" "default-html.html"))

这样配置后,创建一个以 .html 为后缀的文件会插入 ~/.emacs.d/templates/default-html.html 的内容. 这个功能很不错,不过对于资深用户还不太够用.

4 Combining YAS and Auto Insert

我们可以用一个模板作为新文件的默认内容, 这样我们还可以对插入的样板作一些修改.

YAS实际上使用 yas-expand-snippet 来完成扩展动作的, 这个函数接受一个参数,那就是要插入模板的内容. 你可以将下面代码放入 *scratch* buffer中,然后执行这条语句试试(用C-x C-e)来执行:

(yas-expand-snippet ";; Bah-da $1 Bing")  

你大概能够猜到我下一步要干嘛了对吧? 让我们来创建一个辅组函数,这个辅组函数将auto-insert自动插入新文件的内容作为模板来进行扩展.

(defun autoinsert-yas-expand()
  "Replace text in yasnippet template."
  (yas-expand-snippet (buffer-string) (point-min) (point-max)))

上面 (buffer-string) 会返回buffer的整个内容, 而yas-expand-snippet接受的额外两个参数指明了用结果替代当前buffer的哪些内容. 在上例中的 (point-min)(point-max) 表示替换整个buffer的内容.

define-auto-insert 函数能够接受一个数组为参数,数组中的元素若为字符串,则表示引入相应文件的内容,若元素为一个函数名称,则表示执行该函数:.

(define-auto-insert "\\.el$" [ "defaults-elisp.el" autoinsert-yas-expand ])

上面的设置表示,当新建一个以 .el 为后缀的文件时,先插入 defaults-elisp.el 文件中的内容,然后执行函数 autoinsert-yas-expand,这个函数会扩展该模板并替代原模板的内容.

你甚至还可以在模板中添加 $1 , $2 这样的域占位符.

我是用use-package来封装这些模板的,像这样:

(use-package autoinsert
  :config
  (define-auto-insert "\\.el$" ["default-lisp.el" ha/autoinsert-yas-expand])
  (define-auto-insert "\\.sh$" ["default-sh.sh" ha/autoinsert-yas-expand])
  (define-auto-insert "/bin/"  ["default-sh.sh" ha/autoinsert-yas-expand])
  (define-auto-insert "\\.html?$" ["default-html.html" ha/autoinsert-yas-expand]))

5 Programmatic Snippets

手工输入域的内容当然可以,不过若是能用程序自动输入某些信息不是更好吗?

比如, 一般来说,我们的Emacs Lisp文件头部都是这样的:

;;; demo-it --- Utility functions for creating demonstrations
;;
;; Copyright (C) 2014  Howard Abrams
;;
;; Author: Howard Abrams [[mailto:howard.abrams%2540gmail.com][<howard.abrams@gmail.com>]]
;; Keywords: demonstration presentation
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; ...

这里第一行包含了文件的名称及其描述. YAS会将反引号中的代码作为Emacs Lisp来执行,因此执行:

(yas-expand-snippet "`(buffer-file-name)`")  

会插入buffer所示文件名的完整路径, 而执行:

(yas-expand-snippet "`user-full-name`")  

会插入变量 user-file-name 的值.

我们的Emacs Lisp模板可以设置成这样:

;;; `(upcase (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))` --- $1
;;
;; Author: `user-full-name` <`user-mail-address`>
;; Copyright © `(format-time-string "%Y")`, `user-full-name`, all rights reserved.
;; Created: `(format-time-string "%e %B %Y")`
;;
;;; Commentary:
;;
;;  $2
;;
;;; Code:

$0

;;; `(file-name-nondirectory (buffer-file-name))` ends here

6 Full Programmatic Inserts

我的日记文件存放在 ~/journal 目录中,而且日志文件的名字就是 YYYYMMDD 格式的时间. 我们可以会尝试创建一个类似这样的模板来自动插入标题:

#+TITLE: Journal Entry for `(format-time-string "%e %B %Y")`

不过这要求我能够每天都准时地写日记才行. 更好的方式应该是根据文件名来插入标题. 我们可以这样来定义日期格式:

(setq org-journal-date-format "#+TITLE: Journal Entry- %e %B %Y")

然后定义一个函数来解析 buffer-file-name 并填充上面定义的日期格式:

(defun journal-title ()
  "The journal heading based on the file's name."
  (interactive)
  (let* ((year  (string-to-number (substring (buffer-name) 0 4)))
         (month (string-to-number (substring (buffer-name) 4 6)))
         (day   (string-to-number (substring (buffer-name) 6 8)))
         (datim (encode-time 0 0 0 day month year)))
    (format-time-string org-journal-date-format datim)))

现在,我们的模板可以改写成:

#+TITLE: Journal Entry for `(journal-title)`

太棒了, 不过我们还可以更近一步…

我非常热衷于Habitica, 我一直在尝试将它与Emacs结合的更紧密些, 我好喜欢它的日常任务这个设计,我每天完成它们,然后它们在第二天又出现了.

我已经有了一些好用的获取任务 的代码, 但是它做不到每天重复这些任务. 也许,我可以试试用我的每日日记来追踪这些任务.

只有在我创建的是今天的日记时才需要插入这些日常任务. 而且每天的日常任务可能还不一样.

我可以直接在YAS模板中插入相关实现,但是这样一来 `(...)` 中的代码会掩盖掉普通的文本结果,因此还是将它分解成一些小的模板好了:

  • journal-dailies.org 包含的是实际的日常任务to contain the real dailies
  • journal-dailies-end.org 包含的是后面的笔记
  • journal-mon.org 包含的是周一日记的额外内容
  • journal-tue.org 包含的是周二日记的额外内容
  • 以此类推 a journal-XYZ.org 表示的周N的外内容

有了这些文件,编辑我的日常任务列表就很直观了.

现在我需要更改一下我的目标了. 既然我需要创建一系列的辅组EmacsLisp函数,那我不如创建一个整体的函数来生成内容好了.

(define-auto-insert "/[0-9]\\{8\\}$" [journal-file-insert])

当我新建一个仅仅由8个数字组成的文件时,就会调用函数 journal-file-insert:

(defun journal-file-insert ()
  "Insert's the journal heading based on the file's name."
  (interactive)
  (insert (journal-title))
  (insert "\n\n") ; Start with a blank separating the title

  ;; 若创建的刚好是今天的日记
  (when (equal (file-name-base (buffer-file-name))
               (format-time-string "%Y%m%d"))

    ;; Note: `insert-file-contents' 函数会保持光标的位置在插入内容的前面,因此我们这里需要按相反的顺序以此插入文件内容
    (insert-file-contents "journal-dailies-end.org")
    (insert "\n")

    ;; 插入那些每周只会发生一次的任务
    (let ((weekday-template (downcase
                             (format-time-string "journal-%a.org"))))

      (when (file-exists-p weekday-template)
        (insert-file-contents weekday-template)))

    (insert-file-contents "journal-dailies.org")
    (previous-line 2)))

我对Auto Insertyasnippet project 的了解就这么多了. 你们有什么问题或者技巧可以分享的么?