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

发布于 2024-11-25 22:56:48 字数 9668 浏览 20 评论 0

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

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

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

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

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 模板中插入相关实现,但是这样一来 (...) 中的代码会掩盖掉普通的文本结果,因此还是将它分解成一些小的模板好了:

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

现在我需要更改一下我的目标了. 既然我需要创建一系列的辅组 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 的了解就这么多了,你们有什么问题或者技巧可以分享的么?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

无声情话

暂无简介

文章
评论
26 人气
更多

推荐作者

笑脸一如从前

文章 0 评论 0

mnbvcxz

文章 0 评论 0

真是无聊啊

文章 0 评论 0

旧城空念

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文