目录

Emacs 自力求生指南 ── 前言

1 Emacs 是什么?

就我理解, Emacs 是一个长得像文本编辑器的 REPL

  • 它是 Emacs lisp ──一个 Lisp 方言──的运行时。
  • 每次你打开 Emacs ,它都会使用一个干净的1运行时跑一遍 ~/.emacs.d/init.el 脚本。2
  • 你所看到的内容(窗口、文字、状态栏、光标)以及与它的互动(键盘、鼠标)所造成的内容改变,均为上述脚本执行函数所产生的 副作用

2 缺点?

先说缺点吧,客观一些。

2.1 学习成本高

想要随心所欲地使用 Emacs 的话,不可避免地需要用 Elisp 深度定制。

推荐参考书是 ANSI Common Lisp 指南

🤔️ ???

把 Emacs 当作 CL 的 REPL 基本没啥问题,只要 (require 'cl) 就可以用 Common Lisp 的关键字了。

这么想,你在用一个编辑器的时候顺便还能把 LISP 大家族 Racket Clojure ( ClojureScript )、 Scheme 都入了个门,何乐而不为啊((

2.2 有些操作会阻塞编辑器

虽然 Emacs 26+ 的 async 已经实现得很好了,日常操作基本不会被阻塞。但你依然还是可能会被什么套件里的同步调用卡一下。比如

  • 打开一个 parse 特别费劲的文件,比如一个超大的单行 JSON
    • tree-sitter 完善后这将不会成为一个问题,应该……
  • 有些网络 IO,比如 gnus 下载新闻
    • 现代的前后端分离型软件(比如 telega )不会有这个问题
  • 开一个大图片预览 Buffer 或打开一个 PDF
    • 惊人的事实: Emacs 通过把 PDF 转成图片来预览。所以如果你对一个上百 M 的大 PDF 做缩放操作会十分酸爽……

虽然上述都能被 C-g 打断,不过嘛,没有一个工具是万能的。如果你觉得有个功能 Emacs 干得不够好,那就立刻换一个工具吧。时间宝贵。

2.3 不够轻量,导致默认装机量不够

这个是真的没办法了… vi (不是 vim )几乎是每个服务器 Linux 的标配,但 Emacs 的基础包实在太大3,甚至不少桌面版 Linux 都不会预装它。

不过 macOS 居然预装了它,难道帮主爱用?

2.4 前后端互动麻烦

Emacs 作为前端与后端进程通信的场景,在极端情况下,性能不理想:后端 burst 大量数据时可能会卡死 Emacs ,比如 LSP 后端一次性给了太多的补全建议之类的。

如果写 Emacs 的外部动态库,弹性就不太够。

3 长处?

3.1 万物皆文本

除了状态栏(mode line)外4,所有你看到的文字都能使用统一的逻辑、统一的环境来互动:

  • 你在写文档 (org-mode) ,文档里需要插入一段 C 。org-mode 提供一个函数,把这段嵌入代码映射到一个新的子窗口。你可以在这个子窗口里享受所有编辑 C 项目时你所使用的工具和环境,比如代码补全(LSP 和 company-mode)、预定义的代码片段( yasnippet )、语法查错( flycheck )等。

  • helm grep 可以搜索整个 Project ,搜索结果呈现在一个 Buffer 里。你可以 直接修改这个 Buffer 并「保存」 。同样,修改过程中你可以使用所有你早已熟悉的文本处理工具和流程,比如可视化正则文本处理器 anzu 、多光标 multiple-cursors.el ,甚至临时写个 elisp 函数并当场执行也可以。

  • Emacs 自带的 dired 是一个文件浏览器。同样,你可以在它的 buffer 里「 直接修改并保存 」。从此批量更名再也不用找额外的软件或者记额外的命令。注意图中右下角的 Editable

  • eval-expression (默认 M-: ) 可以把最底下一行(minibuffer)变成一个临时的 Elisp REPL,这里你可以执行任何 Elisp 函数,结果也会回显在里面。哪怕这个输入框只有一行高度,你会发现编辑体验和编辑一个 .el 文件是一致的:都有括号配平、都有函数名补完、一样能使用片段展开,甚至还能继续用 C-x C-e 来「临时执行表达式的一部分」。

    这一行的可订制性和大 buffer 是一模一样的,很多软件,诸如 ivy 或者 smex ,都把这一行玩出了花。

3.2 文本皆结构,编辑文本实为操作结构

首先我强烈建议你花三分钟 看看这个视频

然后来重温一下这个经典的小故事:

转载自译言网 | http://article.yeeyan.org/view/legendsland/209584

在 ILC 2002 大会上,前Lisp大神,当今的Python倡导者 Peter Norvig,由于某些原因,做一个类似于马丁路德在梵蒂冈宣扬新教的主题演讲,因为他在演讲中大胆地声称Python就是一种Lisp。

讲完后进入提问环节,出乎我意料的是,Peter点了我过道另一侧,靠上面几排座位的一个老头,他衣着皱褶,在演讲刚开始的时候踱步进来,然后就靠在了那个座位上面。

这老头满头凌乱的白发,邋遢的白胡须,像是从旅行团中落下的游客,已经完全迷路了,闲逛到这里来歇歇脚,随便看看我们都在这里干什么。我的第一个念头是,他会因为我们的奇怪的话题感到相当失望;接着,我意识到这位老头的年纪,想到斯坦福就在附近,而且我想那人也在斯坦福 —— 难道他是……

“嗨,John,有什么问题?” Peter说。

虽然这只是10个字左右的问题,我不会假装自己记住了Lisp之父约翰麦卡锡说的每一个字。他在问Python程序能不能像处理数据一样,优雅地处理Python代码。

“不行。John, Python做不到。”

Peter就回答了这一句,然后静静地等待,准备接受教授的质疑,但老人没有再说什么了。此时,无语已胜千言。

什么叫「像处理数据一样处理代码」?我们知道整个 Emacs 都是用 Elisp 构建起来的,而 Lisp 的迷人之处就在于「代码即数据,数据即代码」: List 在没被求值之前是数据,被求值时就成了代码。

视频里使用的那些快捷键和函数,与其说是文本操作,不如说是操作了语法树后,又重新渲染回 buffer 文本。所以在 Emacs 里写 Lisp 、写 Clojure 、写 Elm 是非常非常享受的事情,心智负担和操作负担都比其它抽象语言好得多。

不要去玩那些括号玩笑了,差远了,用 paredit 写 lisp 根本不需要数括号,哪怕把括号全隐藏都能写出 valid 的程序。

代码是什么?是文本。数据是什么?是结构。「文本即结构」的血脉流淌在 Emacs 的各个角落,除了写 Lisp 之外:

  • paredit-everywhere 可以把「编辑语法树」的思想扩展到几乎所有程序语言上

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
    # 举一个例子, || 表示光标所在位置
    defmodule Test do
    ||def abc do
        "Hello World"
      end
    end
    # C-k 为「删除到行尾」。
    # 在上述光标所处位置,一般版本的 C-k 会立刻打破 do...end 平衡
    # 如果使用 paredit-everywhere 提供的 paredit-kill 的话:
    defmodule Test do
     ||
    end
    
    # 如果光标在引号里呢?
    defmodule Test do
      def abc do
        "Hello ||World"
      end
    end
    # paredit-kill 后:
    defmodule Test do
      def abc do
        "Hello ||"
      end
    end
    
    # 其它括号也是一样
    defmodule Test do
      def abc do
        some_array = ||[
          "1",
          "2",
          "3",
        ]
      end
    end
    # paredit-kill 后:
    defmodule Test do
      def abc do
        some_array = ||
      end
    end
    
    # 我直接把 paredit-kill 绑定在 C-k 了。没这功能我写不了程序。
    
  • org-mode 里的 org 指的是 organized plain text ,就是「文本即结构」的最直接体现。比如

    • org-refile 会把整个标题及其所属内容移动到另一个标题之下,期间所有的级别变化、缩进都会自动完成
    • 调整标题或列表的上下顺序使用 M-upM-down ,同样是以整个结构为单位的移动
    • 每个元素都分配有自己的 UUID。创建链接使用 UUID ,哪怕目标元素事后改变了位置或内容也不怕
    • 表格明明是纯文本写的,操作起来却和 Excel 差不多,甚至还能写自动计算公式
    • org-capture 可以快速往表格里 append 一行数据(而不用操心这个表格的边框有没有被打断之类的)
    • 甚至还有个 类似 SQL 的软件 可以以复杂的条件组合来查询你的文档库。

3.3 GUI 友好、鼠标友好、不反直觉

奇怪的是没几个 Emacs 介绍文提到这个的:Emacs 鼓励你使用它的 GUI 模式。

  • 无参数启动 emacs 就是 GUI5
  • 自带了可深度自定义的 Menu 和 Toolbar
    • 大部分常用功能都能在菜单栏里找到,甚至还能显示当前快捷键组合。前期我建议你不要关掉菜单栏,找个功能还是相当方便的……
  • 鼠标的框选、滚轮、双击、右键菜单等操作和你的使用习惯一致
  • 外观、字体的颗粒度极细。可以使用多套字体、所有桌面色彩和花哨的 window decoration
  • 甚至 Emacs 本身有一个类似控制面板的 GUI 配置界面 M-x customize ,可以不写 Elisp 、不碰配置文件也能轻度定制编辑器行为
  • 看图、浏览网页、刷 Telegram 、预览 Markdown 等场景几乎只有在 GUI 内才 make sense

3.4 内置官方唯一指定软件包管理器

还是可视化的。可以直接点击 Install 按钮安装。

3.5 天生支持 C/S 模式

你肯定有过想在打开两个 vim 进程间互通剪贴板或光标互相跳转的场景,遗憾的是,不能。6

Emacs 能以 server 模式启动, expose 到端口或 socket 文件。client 能随时连接它7,还能主动抢占焦点。

3.6 文档又多又全还易读

M-x info 里的文档每一篇都可以拿来当小说读。

3.7 随时 Hack ,彻底 Hack

所有可见元素和变化都是 执行函数 所带来的副作用,所以你对编辑器的改造几乎没有场所和功能限制。

来几个例子体验一下 Emacs 的可定制性吧。这些例子很糙,接下来几章会更加系统。

3.7.1 自己造一个简单的 Vim 按键模式

在 Emacs 中,「按 j ,一个字母 j 出现在 Buffer 里」也是 函数调用带来的副作用

C-h k j 可以看到调用的函数叫 (self-insert-command)

我们试着重新绑定 j 让它变成「跳到下一行」。

  1. 我们只知道按方向键下可以跳到下一行。首先用 C-h k (方向键下) 来查询对应的函数调用。我们可以得到很多信息:

    • 函数调用是 (next-line)
    • 方向键下在 Emacs 里写作 <down>
    • 这个函数也被绑定到 C-n 上了。
  2. *scratch* Buffer 里试试看:

    1
    2
    3
    
    (global-set-key ;; 全局绑定设置
     (kbd "j")      ;; 按键 j
     'next-line)    ;; 调用 (next-line)
    
  3. 把光标移到最后一个括号的后面,按 C-x C-e(eval-last-sexp) ),现在按键盘上的 j ,你会发现光标真的向下跑了。

  4. 以此类推把 hjkl 都绑定了吧(

我们这个例子太糙,执行了这个之后你的 j 就再也打不出字来了,最方便的复原法就是重启 Emacs……

这个例子表明 Emacs 不仅能做 Vim 能做的任何事儿,而且甚至能做得更好。

事实上,Emacs 的 evil-mode 是我用过的最接近 vim 原生的 vim style 实现。

3.7.2 自定义自己的副作用函数

将光标向左移动一格,你会用 C-b 。我现在想写一个函数,让我能一次向前移动三格。

模拟击键 C-b 三次吗?不够鲁棒,万一有用户把它绑定到其它功能了怎么办。8

那么我怎么精确定义这个函数呢?

  1. 使用 C-h k C-b 查询 C-b 究竟绑定了什么函数。结果是 (backward-char)

  2. 试试看,在 M-x 里使用 backward-char ,发现光标真的回退了一格

  3. 顺便在这个帮助文档里还看到了 (backward-char) 可以加一个参数用来表示回退几个字符

  4. 可以动笔写了:

    1
    2
    3
    4
    
    (defun my/backward-3-chars ()   ;; 该函数没有参数
      "Backward 3 chars."           ;; Docstring。以下是函数本体
      (interactive)                 ;; 该函数可被 M-x 调用或绑定快捷键
      (backward-char 3))
    
  5. *scratch* Buffer 里粘贴这一段9,把光标移到最后一个括号的后面,按 C-x C-e(eval-last-sexp) ),你会看到状态栏里出现了一个 my/backward-3-chars ,说明 defun 成功了10

  6. 试试在 M-x 里调用 my/backward-3-chars ,works as expected.

  7. 不妨把它绑定到一个快捷键上?

    1
    
    (global-set-key (kbd "C-M-b") 'my/backward-3-chars) ;; Ctrl + Alt + b
    
  8. 把这些代码放到我的配置里,就能每次打开 Emacs 自动生效啦。


  1. 其实带了一些 Emacs 预设的默认值。比如没有 ~/.emacs.d/init.el 文件时 Emacs 也依然能生成一个窗口来。 ↩︎

  2. Emacs 28 之后变成 $XDG_CONFIG_HOME/emacs/init.el 了,一般是 ~/.config/emacs/init.el ↩︎

  3. 如果精简太多就会让 Emacs 失去太多功能,最后变成跟 nano 差不多的存在…… ↩︎

  4. 但是这并不意味着状态栏无法定制了。事实上,不仅状态栏软件包 多如牛毛 ,而且甚至还有能 把状态栏和 minibuffer 二合一的软件 ,只能说定制 Emacs 是没有极限的。 ↩︎

  5. 也有 CLI 模式: emacs -nw ↩︎

  6. neovim 在 C/S 模式上做了不少努力,その努力を認めよう。 ↩︎

  7. 具体参考 emacsclient --helpC-h i m Emacs server 。简单地说,启动服务器是 emacs --daemon ,启动客户端是 emacsclient ↩︎

  8. 虽然不太可能,但因为 Emacs 什么都能做,所以如果真的有人这么干了,也请不要奇怪: Because he can. ↩︎

  9. 如果你用 Emacs 打开 org 格式的本文的话,你可以直接把光标放在 BEGIN_SRCEND_SRC 内按 C-c C-c ,这段会自动执行,并将结果追加到这个代码块后面。 ↩︎

  10. 状态栏里的显示是 (defun) 函数的求值结果:一个名叫 my/backward-3-chars 的 Symbol。如果你在 (+ 1 2) 的后面按 C-x C-e ,你会看到求值结果 3 。 ↩︎