Parentheses Universalistic!
创作动机
相信很多 Emacser 编程的时候都离不开 paredit 或类似插件。我在之前的几年中试过两个这类插件:
-
awesome-pair:
-
优点:在很多编程语言,特别是 HTML 模板中表现非常好。
-
缺点:部分行为不是很符合我的习惯,而且缺少软删除一个单词的命令。
-
-
smartparens:
-
优点:支持的语言很多,而且有
sp-forward/backward-kill-word
,这样就覆盖了日常用的删除一行/一个词/一个字的命令,而且在删除选区的时候也会做判断,基本保证了怎么都不会搞坏语法。 -
缺点:在多年的架构变迁中,HTML 相关的功能慢慢坏掉了,并且一直都没有人修,导致基本无法在
web-mode
中使用。其他的工作也积压了很多而不去处理。我认为这体现出这个项目的状态不是很健康。
-
经过长期使用,我发现我只需要两个功能:
- 自动配对括号
- 软删除一个字、一个词、一行、一个选区等的命令。
其他那些操作表达式的花哨命令(slurp、barf 之类)就无所谓了。Emacs 内置的 electric-pair-mode
已经解决了 1,所以我就做了 Puni 来解决 2。
效果展示
-
在 Lisp 中软删除行:
-
在 LaTeX 文档中软删除行:
-
在 HTML 模板中软删除行(
web-mode
): -
在 shell 脚本中软删除行(
sh-mode
):注意 Puni 知道那个右括号其实不需要配对,所以很愉快地把它删了。
定制性
所有 Puni 的内置命令都是用 puni-soft-delete-by-move
这个 API 定义的。它很容易使用,你可以用它定制符合自己使用习惯和品味的软删除命令。
我们以 Lisp 为例,|
表示光标位置:
|foo (bar
baz)
你可以定制一个「软删除一行」的命令,里面像这样调用 API:
(puni-soft-delete-by-move
#'end-of-line 'strict-sexp 'within 'kill 'delete-one)
它的意思是:软删除到 end-of-line
会把我们带到的位置,但不要超过它(within),删除的部分存到 kill-ring
中(kill)。如果在 end-of-line
之前没有完整的表达式可以删,那就删掉一个表达式 (delete-one)。
敲一次以后变成:
|(bar
baz)
再敲一次以后删干净。
或者也可以换换风格:
(puni-soft-delete-by-move
#'end-of-line 'strict-sexp 'beyond 'kill)
beyond 表示「删到 end-of-line
或着超过它的位置」,这样就可以一次删干净。
又例如:
baz (foo bar)|
你可以定义不同风格的「向后删除一个单词」的命令,可以做到下面所示的所有效果:
baz (foo |bar)
baz (foo |)
baz |
详细请读 README。
通用性
Puni 最大的秘密是它没有包含任何与特定语言相关的逻辑。
也就是说,我没有为你刚刚看到的那些 web-mode
, tex-mode
和 sh-mode
中的神奇效果写任何代码。
那么 Puni 是怎么工作的呢?事实上,Emacs 内建了 forward-sexp
函数。理想地,这个函数会带我们前进(或者后退,如果参数是 -1
)一个平衡表达式,并且在无法继续移动(碰到表达式边界)时停下来。于是,我们可以很轻松地知道不破坏语法的那个边界在哪里,Puni 的关键问题就解决了。
Major mode 可以通过定制 forward-sexp-function
来提供它们自己的 forward-sexp
。不幸的是,这些函数的实现千奇百怪,很多都只能部分地工作。最显著的问题就是它们可能只会寻找「下一个平衡表达式的开始/结束」,而不管从这里是不是能到达那个地方。比如在 web-mode
中:
<|a href="example.com">click me</a>
<!-- 使用 `forward-sexp` -->
<a href="example.com">|click me</a>
可以看到它直接找到了 tag 的结尾。而且从光标到这个地方需要跨过一层尖括号,所以实际上是不能删除到那里的。
Puni 的主要工作是以系统、通用的方式修正了它们的行为,通过:
- 连续在不同方向上调用
forward-sexp
- syntax table
- 常识
- 不做语言特定的处理
基于此,Puni 提供了一对通用的 puni-strict-forward-sexp
和 puni-strict-backward-sexp
函数,这些函数和我们期望的一样:只会跨过面前的平衡表达式,在不能继续前进时会停下来。Puni 就是在这两个函数上建造起来的。
由于 Puni 采用了这样一种通用的方案,它自动支持许多 major mode。只要 major mode 提供的 forward-sexp-function
不算太差,Puni 都可以工作。在最差的情况下,major mode 没有提供这个函数,那么 Puni 会用 Emacs 内置的那个,可以在 ()
,[]
,{}
这样的括号以及字符串中正常工作,这对很多语言来说也很够用了。
结语
我在这篇帖子里讲了很多枯燥的技术细节。所以如果你能读到这里,感谢你
不过话说回来,它们也并不是全无必要。因为 Puni 和所有已有的插件都不一样,我希望能清楚地说明它们究竟有何不同之处,这样可以吸引和我有相同品味的人来用 Puni。
我已经认识到我编程时需要的核心工具就是两个:一个帮我理解代码,一个帮我结构化地编辑代码。这两件事我都用自己的轮子解决了,Citre 解决了前者,Puni 解决了后者。所以我现在感觉很开心