Meow: 与 Emacs 结合得最好的模态编辑系统

1. Intro

Meow 是一个 emacs 下的模态编辑插件,与 evil 类似。模态编辑的思想就是把文本的插入单独拎出来,其他复杂的文本操作放在另外的模态下用单字母的快捷键完成。这可以减少每个操作的按键次数,达到高效快速编辑的目的。以移动命令举例,在 emacs 中,向前移动一个字符的快捷键为 C-f ,但在 vim 中,或者 meow 中,只要切换到 Nornal 模态中,按一个键 f 就可以了。

我们现在来考虑,这两种编辑方式哪一种更有优势。

如果文本插入和文本编辑之间相互穿插的很频繁,那么来回切换模式就很麻烦,这时候用 emacs 的编辑方式就很方便,可以省去很多模态切换的操作;如果文本插入和文本编辑之间切换的时间间隔很长,这时候模态编辑方式就更方便,一次模态切换,可以省去很多的 modifier-key 的按键操作,高效又省力。

所以结合这两种方式才是最好的,这也是 evil 这个插件设计的原则,不要把 emacs 和 vim 放在对立面上,而是把各自的优点结合在一起。这是一种很好的实用主义的观点和设想。不过 evil 更偏重于引入 vim 的编辑方式到 emacs 世界中,但没有太考虑如何更好的结合这两种方式,这些需要你自己去思考和设计。所以安装 evil 后,如果不经过快捷按键的精心设计,那么则会导致按键配置的冗余和混乱。比如我刚开始用 evil 时,引入了 evil 的 leader-key 的按键配置的同时也保留着 emacs 的原始按键配置,经常一个命令对应多个快捷按键,增加了很多记忆负担,也容易引起编辑时的选择困难。

如何结合 emacs 和模态编辑呢,一种简单的想法是,在插入模态中,保留 emacs 的编辑命令,这有点像 spacemacs 的 Hybrid 模态,这样在实际编辑时,就可以根据当前编辑任务的具体情况,选择切换到 Normal 模式进行集中编辑,还是就在 Insert 模态中直接用 emacs 的编辑命令编辑,然后继续插入。

Meow 没有 vim 的包袱,更加纯粹,而且在设计之初就考虑了如何与 emacs 的按键设计兼容与结合。所以引入 meow 并不会增加多余的记忆负担和按键冗余。同时,它不用考虑 vim 的兼容问题,所以可以只配置自己需要的按键。

如不是考虑到放弃 vim 的成本,可能自己早就迁移过来了,看这次学习的情况如何吧,如果顺利,后面就完全迁移到这个上面来。

1.1. Normal模态

  • 移动

Normal模态下的基于字符的移动,与 vim 一致,使用 h,j,k,l

h move left
j move down
k move up
l move right
  • 删除
d 向前删除一个字符
D 向后删除一个字符

1.2. Insert模态

i 进入 Insert 模态,按 ESC 键退出 Insert 进入 Normal 。一些其他进入 Insert 的快捷方式

i Insert cursor before the selection
a Insert cursor after the selection
I Insert new line above the current line
A Insert new line below the current line
m a Insert cursor at the start of the line
x a Insert cursor at the end of the lin

其中 m 是向后选择当前行的开头直到非空行的结尾, a 是在当前选择后面插入,组合在一起就是在当前行的开头插入。同样 x 是向前选择当前行到其结尾,然后 a 在选择后插入,结合起来就是在当前行结尾处插入。

在这里可以看到 meow 的编辑模式,选择 + 动作。这一点跟 vim 刚好是反过来的。

1.3. 选择命令

除了上面的 xm 外,还有其他一些选择命令,跟 vim 一样,也是按照语义来定义的:

e 向前选择,直到当前word末尾
E 向前选择,直到当前符号末尾
b 向后选择,直到当前单词开头
B 向后选择,直到当前符号开头
s 删除当前选择,如果当前没有任何选择,则删除当前位置到该行结尾,类似于emacs的C-k
d 删除当前char
w 选择当前光标下的整个单词
W 选择当前光标下的整个符号

符号只用空格隔开,单词可以用其他字符隔开。

1.4. 扩展选择

当按了上面选择命令后,可以接着进行选择扩展,比如按了 w 选择当前单词,在按 e 向前继续扩展选择到下一个单词末尾。大致模式是,选择命令后面继续接选择命令为扩展选择区域。

可以按 ; 调转光标在选择区域中的方向, z 是 undo 之前的选择, g 是清楚当前所有选择。

1.5. 依对象扩展选择区域

除了上面的选择扩展,还有更加通用的基于对象的扩展选择操作:

[ expand before cursor until beginning of…
] expand after cursor until end of…
, select the inner part of…
. select the whole part of…

这些操作后面接不同的对象,完成不同编辑区域的选择:

r round parenthesis
s square parenthesis
c curly parenthesis
g string
p paragraph
l line
d defun
b buffer

Meow 为这些操作提供了方面的操作界面,在 Normal 模态按下操作,比如 ] ,meow 会马上弹出对象选择菜单,而不用完全把这些对象记住,其在可用性方面做得比 vim 更好。

Meow 的在做选择操作的同时也移动光标,比如要移动到整个 buffer 的开头,可以按 [b ,这对应于 vim 的 gg 命令,以及 emacs 的 M-< 命令。但这种统一选择移动的设计,让这种方式更容易理解和记忆,这是 vim 和 emacs 之前所缺失的。

要回到之前的位置,只需按 z 取消之前的选择就可以了。

注意,有些对象必须在对应的 major-mode 中才有用,比如 defun 对象,一般而言要在 prog-mode 中才能正确识别。

1.6. Find/Till

t select until the next specific characte
f same to t, but jump over that character

这个命令类似于 vim 的 t,f 命令,向前跳转到某个字符,可以通过 - 指定向后的方向,在 vim 中则是用大写字母 T,F 来向后查找。用 - 这种方式指定方向的思想来自 emacs 的 universal-args,这是 meow 结合 vim 和 emacs 两个世界的优势的一个例证。

在 emacs 中要完成这些操作,需要调用 zap-uo-to-word 命令,一般绑定到 M-z ,可以通过 C-u - 或者 M-- 来指定方向。

与 vim 和 emacs 比较而言,meow 的方式更加简洁高效。

1.7. 修改命令

c delete the current selection and switch to Insert mode
d i 与 c d 等同
s kill,剪切复制当前选择区域
y yank,复制当前区域
p 在当前光标插入复制内容
u undo
U 只 undo 当前选中区域的内容

1.8. BEACON (BATCHED KEYBOARD MACROS)

Meow 这种先选择后操作的模式,可以让其执行 multi-editing 像内置操作一样容易。

  1. 首先选择一块区域,可以通过任何选择操作
  2. G 进入 BEACON 模态
  3. 执行选择(子选择),修改操作(这会在所有子区域同时执行修改操作)。
  4. 退出Insert模态,并按 G 退出 Beacon 模态
  5. 完成编辑

这是 meow 与 vim 之间最大的不同,其强大简洁的 multi-editing 能力,是其最吸引我的地方。

1.8.1. Beacon 可以结合 emacs 的 keyboard-macros 系统完成复杂的编辑任务

下面是一个例子:

1 2 3
=>
[| "1" |] [| "2" |] [| "3" |]

具体操作:

  1. 把光标放到例子的第一行,按 x 选择要编辑的行
  2. G 进入 Beacon 模态
  3. b 在每个单词的前段插入伪光标
  4. F3 启动 emacs 的 key-marco 记录
  5. 编辑
  6. F4 介绍宏记录并应用
  7. G 结束 Beacon 模态
  8. 编辑结束

另一个例子:

x-y-foo-bar-baz
=>
x_y_foo_bar_baz

具体操作:

  1. W 选中第一行的整个符号
  2. G 激活第二选择区域
  3. f - 查找每个“-”字符,并在字符“-”处自动插入伪光标
  4. c 修改字符“-”,meow 会自动进行记录
  5. 输入“-”
  6. ESC 退出 Insert 模态,进入 Normal 模态,这时 meow 会在所有伪光标处应用记录
  7. G 退出 Beacon 模态,完成编辑

1.9. 快速访问和搜索

v 启动快速访问,它会在 minibuffer 中提示用户输入要快速访问的符号,然后在当前 buffer 中定位到这个符号,可以通过命令 n 跳转到下一个匹配的符号。可以通过 ; 来改变查找方向。 n 操作会遵循当前方向进行查找。

另外,只要你有选择某块区域,就可以直接用 n 操作进行快速搜索,比如用 w 选中当前单词,然后按 n 进行搜索。

用快速访问实现 emacs 中的 serach-replace 操作:

  1. v 快速访问要查找的单词
  2. c 修改查找到的单词,然后按 ESC 进入 Normal 模式
  3. y 保存替换的内容
  4. n r ,查找下一个并替换

2. Conceptions

2.1. 模态

Emacs 里面分 major-mode 和 mirror-mode,vim 中有 Insert 模态和 Normal 模态,这两者有交叉,但也有很大不同。

在 emacs 中,major-mode 是跟文件类型相关联的,每一种特定的文件类型对应一种 major-mode,其中定义了大部分与这类文件相关的操作和定义。其他一些通用的特性通过很多不同的 mirror-mode 提供,比如 hl-line-mode 可以高亮当前行,display-line-numbers-mode 可以在当前 buffer 显示行号。这种设计赋予了其极强的扩展能力。

Vim 中的模态只针对编辑而言,其中不包括与当前载入 buffer 文档类型相关的东西,因此其扩展性要差一些。不过这不算其缺点,文本编辑本就是其主要业务,而 vim 把这一块做到了极致。更加高效更加细粒度的编辑方式是 vim 的最主要优势。由于其功能内聚,所以其小巧干练,这是它最吸引我的地方。

Meow 是 emacs 极强扩展能力的一个强有力的例证,它结合了 emacs 和 vim 的各自优势。

为了区分 emacs 的 mode 和 vim 的模式,在 meow 中,类 vim 的模式称之为模态, emacs 的 major-mode 和 mirror-mode 叫做主模式和副模式。这两种不同模式结合在一起,使得我们可以根据实际需求,使用各自的功能。以前的 emacs 编辑模式可以看做这种结合下的 Insert 模态,在这种模式下可以直接输入内容,同时也可以用 modifier-key 的方式调用 emacs 的编辑功能。另一方面,在 Normal 模式下,也可以同时调用更简短的 vim 式的快速编辑命令,也可以直接用 modifier-key 的方式或者 leader-key(keypad) 的方式调用 emacs 的命令。

Evil 的目标是在 emacs 上完全模拟 vim,怎么把 evil 整合到 emacs 的工作流中,需要用户自己思考和配置。比如在 evil 中的 Insert 下,为了模仿 vim 中的按键,因此覆盖了许多 emacs 原本的按键,这些冲突的地方,都需要用户自己去留意和配置。也因此,在 evil 中, Insert 模态与 emacs 的编辑模态不一样,为了解决这个问题,又单独增加了一个 emacs 模态,以和 evil 的 Insert 模态做区分。这样在 evil 中就包含了两种输入模态,搞得挺麻烦,而且这种设计还割裂了 emacs 和 vim 的各自优点。在 emacspace 这个 start-kit 发行版中又单独引入 Hybird 模态,来融合这两者。总之,evil 背负太多的 vim 的负担了,导致其有些臃肿。

Emacs 中,除了编辑模式,还有很多 special-mode,比如 dired,ibuffer 等功能所使用的 major-mode 都继承于 speical-mode。因为这些模式中的操作不同于文本插入和文本编辑,所以在 vim 和 meow 中都引入 motion 模态来处理这种情况。

2.1.1. Motion 模态

在 meow 中, Motion 模态默认使用 SPC 作为 leader-key,原本的 SPC 可以通过 SPC SPC 访问,除此之外就再没有绑定其他快捷键了。在 Motion 模态下,如果定义的快捷键覆盖了 emacs 已有的快捷键,那么 emacs 原本的快捷键被重新绑定到 H-<key> 上。比如为了在 Motion 模态中也使用 j,k 进行上下移动,可以通过如下配置达成:

(meow-motion-overwrite-define-key '("j" . next-line))
(meow-leader-define-key '("j" . "H-j"))

访问 emacs 原本 j 绑定的功能,可以通过 H-j ,或者 SPC j .

2.1.2. Keypad 模态

这个模态类似于 vim 的 leader-key,但又很不同,它实现了不用按 modifier-key 来复用 emacs 的 modifier-keybings。在 Normal 模态中按 SPC 进入 Kaypad 模态,然后用户的按键会按下面的规则进行转换:

  1. 首字母除了「x,c,h,m,g」,会被转换成 C-c <key>
  2. m 会转换成 M- ,后接另一个字母,则为 M-<key> ,比如 SPC m h 会转换成 M-h
  3. g 会转换成 C-M-
  4. 中间的 SPC 表示下一个输入没有特殊含义,有点类似转义字符,比如 m g SPC g => M-g g
  5. 其他情况下,输入会转换成 C-<key> ,比如 x f => C-x C-f

这个模式很有用处:

  1. 它可以简化按键,比如上面的(5)中,用 SPC x f => C-x C-f 可以减少一个按键,关键的是少了两次 modifier-key 按键,这对小拇指的健康很重要。
  2. 它复用了 emacs 本身的按键,而不是单独新增一种按键设置,所以在我们新增 key-bindings 时,可以完全按照 emacs 的方式设计 key-bindings,然后就同时获得了一种 leader-key 按键的方式。

这又是 meow 结合 vim 和 emacs 两社区优势的另一个例证。

2.1.3. Beacon 模态

其也叫做 Batch-KMacro,在这种模式下,可以把键盘宏应用到多个地方。

当光标移动到 secondary-selection 中, Beacon 模态会自动启动了;如果光标移出 secondary-selection 或者 secondary-selection 去激活, Beacon 模态自动退出。

当处在 Beacon 模态下,可以通过移动命令创建伪光标:

meow-left/right 在当前列创建伪光标
meow-next/back-word/symbol 在词的开头或者结尾处创建伪光标
meow-mark-word/symbol will create regions for every same words
meow-visit/search will create regions for every same regexp
meow-find/till will create cursors for every same characters
meow-line will create regions for every N lines
meow-join will create cursors for each indentation beginning

一旦创建了伪光标,就可以做如下操作:

  1. 简单进入 Insert 模态(自动启动宏记录),完成编辑退出 Insert 模态(自动结束宏记录,并应用该宏到所有光标或区域处)
  2. 一般的启动宏记录(按 F3),完成编辑,(按 F4)结束宏记录并应用

3. Config

(require 'meow)
(defun meow-setup ()
  (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
  (meow-motion-overwrite-define-key
   '("j" . meow-next)
   '("k" . meow-prev)
   '("<escape>" . ignore))
  (meow-leader-define-key
   ;; SPC j/k will run the original command in MOTION state.
   '("j" . "H-j")
   '("k" . "H-k")
   ;; Use SPC (0-9) for digit arguments.
   '("1" . meow-digit-argument)
   '("2" . meow-digit-argument)
   '("3" . meow-digit-argument)
   '("4" . meow-digit-argument)
   '("5" . meow-digit-argument)
   '("6" . meow-digit-argument)
   '("7" . meow-digit-argument)
   '("8" . meow-digit-argument)
   '("9" . meow-digit-argument)
   '("0" . meow-digit-argument)
   '("/" . meow-keypad-describe-key)
   '("?" . meow-cheatsheet)
   ;; windows
   '("o" . delete-other-windows)
   '("=" . split-window-right)
   '("-" . split-window-below)
   ;; high frequency
   '("e" . "C-x C-e")
   '("<SPC>" . "C-x C-s")
   '(";" . comment-dwim)
   '("k" . kill-this-buffer)
   '("p" . project-find-file)
   '("b" . switch-to-buffer)
   '("f" . find-file)
   '("i" . imenu)
   '("F" . toggle-frame-maximized)
   '("r" . recentf-open)
   )
  (meow-normal-define-key
   '("0" . meow-expand-0)
   '("9" . meow-expand-9)
   '("8" . meow-expand-8)
   '("7" . meow-expand-7)
   '("6" . meow-expand-6)
   '("5" . meow-expand-5)
   '("4" . meow-expand-4)
   '("3" . meow-expand-3)
   '("2" . meow-expand-2)
   '("1" . meow-expand-1)
   '("-" . negative-argument)
   '(";" . meow-reverse)
   '("," . meow-inner-of-thing)
   '("." . meow-bounds-of-thing)
   '("[" . meow-beginning-of-thing)
   '("]" . meow-end-of-thing)
   '("a" . meow-append)
   '("A" . meow-open-below)
   '("b" . meow-back-word)
   '("B" . meow-back-symbol)
   '("c" . meow-change)
   '("d" . meow-delete)
   '("D" . meow-backward-delete)
   '("e" . meow-next-word)
   '("E" . meow-next-symbol)
   '("f" . meow-find)
   '("g" . meow-cancel-selection)
   '("G" . meow-grab)
   '("h" . meow-left)
   '("H" . meow-left-expand)
   '("i" . meow-insert)
   '("I" . meow-open-above)
   '("j" . meow-next)
   '("J" . meow-next-expand)
   '("k" . meow-prev)
   '("K" . meow-prev-expand)
   '("l" . meow-right)
   '("L" . meow-right-expand)
   '("m" . meow-join)
   '("n" . meow-search)
   '("N" . meow-pop-search)
   '("o" . meow-block)
   '("O" . meow-to-block)
   '("p" . meow-yank)
   '("P" . meow-yank-pop)
   '("q" . meow-quit)
   '("Q" . meow-goto-line)
   '("r" . meow-replace)
   '("R" . meow-swap-grab)
   '("s" . meow-kill)
   '("t" . meow-till)
   '("T" . meow-till-expand)
   '("u" . meow-undo)
   '("U" . meow-undo-in-selection)
   '("v" . meow-visit)
   '("V" . meow-kmacro-matches)
   '("w" . meow-mark-word)
   '("W" . meow-mark-symbol)
   '("x" . meow-line)
   '("X" . meow-kmacro-lines)
   '("y" . meow-save)
   '("Y" . meow-sync-grab)
   '("z" . meow-pop-selection)
   '("Z" . meow-pop-all-selection)
   '("&" . meow-query-replace)
   '("%" . meow-query-replace-regexp)
   '("'" . repeat)
   '("\\" . quoted-insert)
   '("<escape>" . ignore)))

(when window-system
  (setq meow-replace-state-name-list
        '((normal . "🅝")
          (beacon . "🅑")
          (insert . "🅘")
          (motion . "🅜")
          (keypad . "🅚")))
  )
(setq
 meow-esc-delay 0.001
 meow-select-on-change t
 meow-cursor-type-normal 'box
 meow-cursor-type-insert '(bar . 4)
 meow-keypad-describe-delay 0.5
 meow-keypad-leader-dispatch "C-c"
 meow-expand-hint-remove-delay 2.0)
(meow-setup)
(meow-setup-indicator)
(meow-setup-line-number)
(unless (bound-and-true-p meow-global-mode)
  (meow-global-mode 1))
(meow-esc-mode 1)

Created with Emacs 29.4 (Org mode 9.6.15) by YangXue
Updated: 2025-01-24 Fri 02:19