让Emacs为你自动插入内容(Emacs模板使用指南)
Table of Contents
我们经常要打字. 而对于文本文件来说,很多的输出只是为了组织内容而已. 比如org-mode中的星号,空格以及 #+
, 比如编程代码中的那些括号, do..end
之类的.
不过也没谁规定这些内容必须是由你手工输入的呀.
下面介绍几种让Emacs为你自动输入的几种方法… 比如我们可以在新建某类文件时自动插入一份样板文.
注意,在本教材中,我不会将关于自动补全的内容,所以不会讲到 auto-complete
或 company
.
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 Insert 与 yasnippet project 的了解就这么多了. 你们有什么问题或者技巧可以分享的么?