让 Emacs 为你自动插入内容 - Emacs 模板使用指南
我们经常要打字. 而对于文本文件来说,很多的输出只是为了组织内容而已. 比如 org-mode 中的星号,空格以及 #+
, 比如编程代码中的那些括号, do..end
之类的。
不过也没谁规定这些内容必须是由你手工输入的呀。
下面介绍几种让 Emacs 为你自动输入的几种方法… ,比如我们可以在新建某类文件时自动插入一份样板文。
注意,在本教材中,我不会将关于自动补全的内容,所以不会讲到 auto-complete
或 company
。
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 还提供了多种触发模板的方法,不过下一步让我们修改模板让它变得更有用一些吧。
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 例子才显得有意义。
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
的内容。这个功能很不错,不过对于资深用户还不太够用。
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]))
Programmatic Snippets
手工输入域的内容当然可以,不过若是能用程序自动输入某些信息不是更好吗?比如,一般来说,我们的 Emacs Lisp 文件头部都是这样的:
;;; demo-it --- Utility functions for creating demonstrations
;;
;; Copyright (C) 2014 Howard Abrams
;;
;; Author: Howard Abrams [<howard.abrams@gmail.com>](mailto:howard.abrams%2540gmail.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
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 的了解就这么多了,你们有什么问题或者技巧可以分享的么?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 压缩和增强手写笔记
下一篇: Android 公共技术点之依赖注入
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论