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. 选择命令
除了上面的 x
和 m
外,还有其他一些选择命令,跟 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 像内置操作一样容易。
- 首先选择一块区域,可以通过任何选择操作
- 按
G
进入 BEACON 模态 - 执行选择(子选择),修改操作(这会在所有子区域同时执行修改操作)。
- 退出Insert模态,并按
G
退出 Beacon 模态 - 完成编辑
这是 meow 与 vim 之间最大的不同,其强大简洁的 multi-editing 能力,是其最吸引我的地方。
1.8.1. Beacon 可以结合 emacs 的 keyboard-macros 系统完成复杂的编辑任务
下面是一个例子:
1 2 3 => [| "1" |] [| "2" |] [| "3" |]
具体操作:
- 把光标放到例子的第一行,按
x
选择要编辑的行 - 按
G
进入 Beacon 模态 - 按
b
在每个单词的前段插入伪光标 - 按
F3
启动 emacs 的 key-marco 记录 - 编辑
- 按
F4
介绍宏记录并应用 - 按
G
结束 Beacon 模态 - 编辑结束
另一个例子:
x-y-foo-bar-baz => x_y_foo_bar_baz
具体操作:
- 按
W
选中第一行的整个符号 - 按
G
激活第二选择区域 - 按
f -
查找每个“-”字符,并在字符“-”处自动插入伪光标 - 按
c
修改字符“-”,meow 会自动进行记录 - 输入“-”
- 按
ESC
退出Insert
模态,进入Normal
模态,这时 meow 会在所有伪光标处应用记录 - 按
G
退出Beacon
模态,完成编辑
1.9. 快速访问和搜索
按 v
启动快速访问,它会在 minibuffer 中提示用户输入要快速访问的符号,然后在当前 buffer 中定位到这个符号,可以通过命令 n
跳转到下一个匹配的符号。可以通过 ;
来改变查找方向。 n
操作会遵循当前方向进行查找。
另外,只要你有选择某块区域,就可以直接用 n
操作进行快速搜索,比如用 w
选中当前单词,然后按 n
进行搜索。
用快速访问实现 emacs 中的 serach-replace 操作:
- 按
v
快速访问要查找的单词 - 按
c
修改查找到的单词,然后按ESC
进入 Normal 模式 - 按
y
保存替换的内容 - 按
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
模态,然后用户的按键会按下面的规则进行转换:
- 首字母除了「x,c,h,m,g」,会被转换成
C-c <key>
- m 会转换成
M-
,后接另一个字母,则为M-<key>
,比如SPC m h
会转换成M-h
- g 会转换成
C-M-
- 中间的
SPC
表示下一个输入没有特殊含义,有点类似转义字符,比如m g SPC g => M-g g
- 其他情况下,输入会转换成
C-<key>
,比如x f => C-x C-f
这个模式很有用处:
- 它可以简化按键,比如上面的(5)中,用
SPC x f => C-x C-f
可以减少一个按键,关键的是少了两次 modifier-key 按键,这对小拇指的健康很重要。 - 它复用了 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 |
一旦创建了伪光标,就可以做如下操作:
- 简单进入
Insert
模态(自动启动宏记录),完成编辑退出Insert
模态(自动结束宏记录,并应用该宏到所有光标或区域处) - 一般的启动宏记录(按 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)