Why Emacs?

Emacs已经陪伴我十几年了,称得上是第二个人生伴侣,只要开电脑就习惯性地打开Emacs。内心由衷地感谢Richard Stallman,开发出这样一款编辑器,给平淡无奇的程序员生活带来许多乐趣,是时候给Emacs写一篇软文了。Emacs诞生于1972年,可以说是相当古老了。现在已经2021年了,这个老古董还能继续用吗?

抛开Emacs不谈,先看看各大主流编辑器的使用情况和变化趋势,Google Trends是一个很好的参考依据,通过Google Trends的数据,我们可以分析出一些结论。

从数据上看,vscode一枝独秀,从2015年发布之后就一直在增长。2015年之前最火的编辑器Sublime Text则在vscode发布之后一直走下坡路。Emacs和vim看起来比较稳定,但是也是有下降的趋势。另外,Emacs的用户基数非常小。

不得不承认,Emacs是非常小众的,而且有变得更加小众的趋势。相对其他编辑器来说,Emacs门槛更高一些,用户上手难,自信心容易受到打击,这是它小众的原因。但是,小众并不意味着它没有价值。在投入一定的精力跨过这个门槛之后,就会觉得原来Emacs有这么多不可替代的功能,希望通过这篇文章,能让更多的人感受到Emacs的这些闪光点。

编辑器的圣战从未停止过,说到编辑器,几乎一定会引来争论,这篇文章可能也会吸引不少的火力。不过说到底都是工具而已,我们也没必要像狂热的宗教信徒一样,极力地吹捧自己用的工具,同时贬低其他的工具。这篇文章也不会试图去说服你,“抛弃其他的编辑器和ide吧,只有Emacs才是最好的”。每个工具都有自己的优势,正确的做法是博采众长,而不是手里有锤子看什么都是钉子。对程序员来说,这也是一个非常重要的原则,千万别把自己封闭在一个固定的技术圈子里面,多了解其他新鲜的事物。比如,在阅读源代码的场景下,Emacs就比不上Jetbrains的IDE,无论怎么配置,Emacs都达不到IDE那种丝滑的体验。另外,通过工具来获得优越感是低级趣味,工具强并不代表你很强。

基本概念

为了让大家理解emacs的使用,有一些基本的概念还是需要介绍一下。

高效率的文本搜索:Swiper

Swiper是Emacs上的一个搜索框架。大家可能觉得,搜索不是最基本的功能吗,这有什么可说的,能搞出什么花样来?大家应该都有这样的经历,有些东西在亲身体验之前无论如何也想象不出它有多么的香,可是一旦用过之后却再也回不去了,比如机械键盘、iPhone的Retina屏幕、python里的requests等等。Swiper也是如此。

Emacs原生的搜索是isearch,与其他编辑器、浏览器等的搜索没什么区别,也是输入搜索的内容,不停的跳到下一个,直到找到需要的内容。occur也是Emacs内置的功能,它比isearch搜索的效率更高了一点。使用occur可以把所有符合搜索条件的行放到一个buffer里,这个buffer是基于Compilation Mode实现的,因此在这个buffer上下移动光标时,原始文件buffer也会跳转到相应的行。

Occur搜索use-package

Occur搜索use-package

Occur的效率提升在于把符合条件的所有行都展示出来,用户可以在这些结果里面进一步的细化搜索,找到目标,并且也可以方便的跳转。但是效率仍然不够高,每次都需要输入文本再执行搜索。如果发现搜错了,或者搜索的文本不够精准需要再改一下,只能从头再来。

Swiper有什么不一样的呢?放一张动图让大家感受一下。

  1. 搜索的候选结果直接在minibuffer展示,不需要开一个单独的buffer。候选结果也同样是可以进行跳转。
  2. 搜索条件可以方便的修改,就像正常的文本编辑一样。并且修改是即时生效的,不需要从头再来。
  3. 支持搜索多个条件,以空格分隔开即可。比如要搜索(use-package smartparens-config)这一行,可以输入usepacksmart3个条件。
  4. 渐进式的搜索体验,一边修改搜索条件一边进行搜索,候选结果列表是逐步减小的。这个特性我觉得是整个Swiper的核心。

有了Swiper的帮助,搜索就不再是什么困难了。以至于公司的同事都很惊讶,还在讨论的某个逻辑时候我已经找到具体代码所在的那一行了。不知道其他编辑器有没有类似的功能,我第一次用的时候确实是被震惊到了,原来搜索还能这样玩。

Swiper的仓库里其实包含3部分:Ivy,Counsel和Swiper。Ivy提供了底层的渐进式搜索框架,这套框架并不仅仅可以用在文本搜索,只要是需要从列表中进行筛选的场景都能派上用场。Counsel基于ivy优化了很多Emacs原生的命令,比如imenu(代码结构大纲)、describe-function(描述elisp函数)等等。

高效率的文件切换

IDE中打开的多个文件一般都是以tab的形式呈现,需要切换就点对应的tab。我一直不能接受这种操作,代码写的好好的,突然手要离开键盘去操作鼠标,思路就被打断了。文件少的时候点两下还可以,数量增多之后就非常蛋疼了。用过chrome的人应该体验过,打开了几十个tab要从里面找一个的时候是多么的绝望。

面对数量众多的文件时,如何快速的找到想要的那个?其实我们需要的是一种指哪打哪的操作,直接切换到脑子里想到的那个文件。上面提到的ivy框架就提供了这样的功能,ivy-switch-buffer。还是来个动图感受一下。

与swiper搜索文本的体验类似,使用ivy-switch-buffer之后会在minibuffer中展示文件列表,可以移动光标进行选择。更牛逼的是,这里可以输入条件来过滤文件。比如我需要找到打开的clojure文件,只需要输入clj即可过滤出来,然后在这个过滤的结果里面找到需要的文件,当然也可以继续输入更多的条件。

ivy-switch-buffer除了可以跳转到已经打开的文件,还可以跳转到最近打开过的文件。这个功能是基于recentf实现的。recentf会自动记录最近打开过的文件,生成一个列表。ivy-switch-buffer会把这个列表也展示在minibuffer中。不过这里只会展示文件的名称,不包含路径。有些场景还是需要路径的,比如不同文件夹下存在同名文件,还是需要按照路径再筛选一下的。这个时候就可以用counsel-recentf了。

使用counsel-recentf查找曾经访问过的metabase目录下的代码

使用counsel-recentf查找曾经访问过的metabase目录下的代码

除了查找最近打开过的文件,有些时候我们也需要打开新的文件。比如找到当前这个git仓库下的某个文件。我们可以用Projectile。projectile支持针对工程内的文件进行操作,常用的比如文件查找、文本搜索等等。projectile支持自定义工程,可以是一个git仓库、svn库,或者是一个目录。

在metabase的工程内查找clojure代码

在metabase的工程内查找clojure代码

通过ivy、recentf和projectile这些插件的组合,实现了高效的文件跳转,写代码的过程变得行云流水,体验极度的顺畅。希望大家看到这里能够对Emacs的效率有一点直观的感受,如果完全看不懂操作,建议还是自己配置一下体验一把,你一定会发自内心的喊出来:“真香啊!”。

扩展、创意与社区

编辑器的效率是它是否好用的标准之一,虽然Emacs原生的功能效率一般,但是它提供了很好的扩展能力,让更多优秀的插件可以组合起来,提供更强大的功能。这就像ios生态一样,苹果提供了app store,剩下的交给开发者就好了。emacs就如同iPhone,这些插件就相当于app。没有app的加持,iPhone也没那么好用。

有时候发现emacs缺少了一个功能,还以为终于有机会搞个自己的插件了,但是google一下就会发现原来已经有人实现过了。另外不得不承认很多插件的开发者都太有创意了,像swiper这样的插件我反正是无论如何都想象不出来的。

我所了解的几个emacs开发者:

  1. abo-abo,就是前面讲的swiper的作者。除了swiper,他还开发了很多个提升编辑效率的插件。比如buffer内快速跳转的avy,快速的window切换ace-window,解决lisp编辑中括号痛点的lispy,支持自定义菜单的hydra。个个都是神器,可以说emacs的体验在他的帮助下至少提升了30%。
  2. bbatsov,projectile的作者,他的博客是Emacs Redux。他还是clojure的插件cider的主要开发者,在cider的帮助下emacs已经是clojure业界的标准工具了。
  3. Protesilaos Stavrou,希腊人,我所使用的主题modus-vivendi的作者。为什么都是程序员,人家就这么有艺术细胞呢?从用emacs开始不知道换了多少个主题了,每个主题用一段时间后就会审美疲劳,但是modus-vivendi就非常耐看,几乎可以一直用下去。
  4. Jonas Bernoulli,magit的作者。magit太好用以至于我现在离开emacs都不会用git了。

Emacs Lisp与学习曲线

下面这个各编辑器的学习曲线图,许多人应该都看过了。图里Emacs的曲线可以在一定程度上反映emacs入门的困难。当然这只是一个戏谑的描述,看看就可以,不要当真。

Emacs使用Emacs Lisp(以下简称elisp)进行扩展,成也萧何败萧何,elisp是Emacs成功的原因之一,但同时也是导致它很难流行起来的主要原因。从最简单的字体、主题的配置,到开发类似ivy这样的插件,都需要用elisp。讲真的对于没有接触过lisp的用户来说,elisp是非常难入门的,即使我有haskell、scala等函数式语言的基础,也觉得elisp里的概念难懂。这就足以劝退很大一部分用户了。

但是,门槛只存在于开始的阶段,如果能跨过这个门槛,别把elisp当回事,随着用户逐步调整配置积累使用经验,就会对elisp越来越熟悉。更关键的是,从入门到骨灰级玩家,用户的成长过程是连续的,只需要用elisp,不用再额外借助其他语言或者工具。作为对比,vscode的基本配置通过json描述,但是如果要实现更复杂的功能,json是支持不了的,还需要用到javascript。

那么emacs真实的学习曲线应该是什么样的呢?我来试着画一下。回顾我使用Emacs的经历,可以分为这么几个阶段:

  1. 小白用户,什么都不会配置,要调整一个配置只能copy其他人的代码。
  2. 蜜月期,学会在Emacs里面安装包,功能逐渐丰富起来,用的也很爽。
  3. 七年之痒,使用体验逐渐趋于平淡乏味,或者遇到某个问题无法解决(其实是能力不足),进而认为是Emacs不行,所以逃离Emacs,转到其他编辑器。
  4. 发现其他编辑器也都有自己不行的地方,还是不忘初心,回到Emacs的怀抱。
  5. 开始自己写简单的Emacs Lisp实现功能。
  6. 发现Emacs Lisp的一些概念非常难懂,但是相关的资料又非常少。因此开始学习Clojure,希望通过Clojure来理解Lisp的概念。总算可以用Emacs Lisp再写一些更加复杂的功能。

如果以时间为横轴,用户所感知到的难度为竖轴,我觉得emacs的学习曲线应该是这样的。

最开始的时候难度相对较低,各种emacs教程都会有一些简单的配置,基本的功能也是可用的。当需要做一些修改时,就会觉得无从下手,难度开始上升。跨过第一个坎解决简单的问题后,也掌握了一些技巧,难度就会逐渐下降。但是后面还会遇到新的问题,需要掌握更高级的技巧才能解决,难度又再次上升。循环往复,每次当你以为已经掌握emacs的时候就会被啪啪打脸。需要经历几次这样的上升和下降,总体的难度才会真的开始下降,最后就会趋于平滑,没什么困难了。

那些觉得emacs太难,以至于被劝退的用户并不会一开始就离开,而是在到达前面这几个山头的时候坚持不下去。这条曲线可以让大家有个明确的心理预期,emacs肯定是有难度的,但是也没有那么的难,不是一座不可逾越的高山。“山重水复疑无路,柳暗花明又一村”,往往再探索一下就能找到解决方案了。在这个过程中用户自身的经验一定是持续上升的,而且在翻过每个山头之后提升的速度都会变得更快。

我用Emacs

我的emacs配置由以上这些文件组成,这1900多行elisp就可以实现对Emacs的全部配置,不再需要其他的任何文件。纯文本格式的elisp代码给人一种一切都在掌握之中的安全感,不像其他的一些ide会导出xml或者二进制的配置文件,只能导出或者导入,没法直接修改,自由度太低。

我用git来进行版本控制,将配置托管在bitbucket上。针对Emacs所做的每一个改动、每一个优化都可以沉淀下来,在日后的每一天里都能发光发热,这不是很牛逼的事情吗?日常工作中会使用多台机器,借助git可以把最新的配置拉回到本地,不管有多少台机器,每台机器上的Emacs体验都是一致的。

虽然我主要用Emacs写代码,但是emacs也可以用来做很多别的事情。

比如,工作中经常需要跑sql排查问题,查询id为某些值的数据。产品和运营反馈的数据一般是excel或者csv,如何把excel里的id列表转换成sql的in语句呢?这里需要做两件事情,一是需要把多行数据转成一行,并且用逗号分隔开,二是如果id是字符串,需要加上单引号。手写也不是不行,但是写多了就会比较浪费时间。为了解决这个问题,我在Emacs里实现了下面这样一个函数。

(defun jxq/join (start end arg)
  (interactive "r\nP")

  (if (not (use-region-p))
      (message "no region is active")
    (let* ((region-str-list
            (s-split "[[:space:]|\n]"
                     (buffer-substring-no-properties start end)))
           (new-str (if arg
                        (s-join "," region-str-list)
                      (mapconcat (lambda (x) (format "'%s'" x))
                                 region-str-list
                                 ","))))
      (save-excursion
        (save-restriction
          (delete-region start end)
          (insert new-str))))))

当id是字符串时,M-x jxq/join即可完成转换,如果是数字类型,C-u M-x jxq/join即可。在没有这个函数之前,我都是在ipython里面处理的,虽然python的代码比这个函数要短,通过splitjoin就能解决,但是这本身就是“编辑”的需求,在编辑器里面做不是天经地义的么?这个功能在编辑器里只需要实现一次,绑定快捷键下次直接按就好了,python还要再从头开始写一遍。

干我们这行的每天都少不了要做一些类似的重复性的工作,每个人的痛点都不一样,但是只要主动从日常的使用场景中挖掘这类需求,再通过emacs解决这些痛点,效率就会不断地提升,生活越来越美好。

入门建议

入门确实有难度,所以我建议通过Spacemacs或者doom-emacs上手。这两个都在原生emacs的基础上做了很多配置和优化,足以为你扫清大部分的入门障碍,体验也完胜原生的Emacs。

用一段时间之后可以尝试从零开始自己配置,或者是研究下他们的代码,知其然还要知其所以然。

最后

有关于emacs想说的实在太多了,再写下去感觉要写成一本书了。几乎可以肯定,Emacs一定不会成为主流的编辑器,但正如我前面所说的,小众并不代表它没有价值。我甚至觉得,在这些年折腾Emacs的经历让我的技术能力都有了很大的提高。比如为了解决某些插件的问题,我掌握了快速阅读源码并定位问题的技能。Emacs也让我对编程语言有了更广泛更深入的认知。Emacs有难度,但同时也带给我很多快乐。写这篇博客的目的就是希望能让更多的人认识emacs,不知道大家看过之后是否有兴趣体验一下?