我是一名熱衷于函數(shù)式編程的Clojurian(Clojure粉),網(wǎng)絡(luò)ID是lambeta(λβ),讀作/‘l?meit?/,個(gè)人的博客網(wǎng)站是https://lambeta.name。俗話說,工欲善其事必先利其器,完善開發(fā)工具與我而言是一件愉快的事情,所以想把經(jīng)驗(yàn)集結(jié)成文字,便有了這篇文章。這篇文章不會介紹太多花式或有深度的emacs配置,更多是摸索學(xué)習(xí)的過程,其中充滿了樂趣。
原因
網(wǎng)絡(luò)上的.emacs.d/init.el配置數(shù)不勝數(shù),各路lisp大神的dot file都已經(jīng)放在github上了,而且前有牛人撰文推薦學(xué)習(xí)emacs配置的詳實(shí)方法,看似確實(shí)沒有什么必要自己折騰一份配置。這個(gè)說法對,也不對。
我在轉(zhuǎn)向emacs之前,是一名忠實(shí)的vim黨,從大學(xué)開始就不斷折騰vim的配置,還花過一段時(shí)間專門學(xué)習(xí)了vimscript,曾經(jīng)驚嘆于vimscript的動態(tài)函數(shù)式風(fēng)格的優(yōu)美和強(qiáng)大。類似地,.vimrc配置文件在網(wǎng)絡(luò)上也多如牛毛,華麗和酷炫的插件極大地提升了vim的操作性。盡管如此,我還是樂于一磚一瓦地打造自己的vim環(huán)境,竭力演化它變成我心目中的“編輯器之神”。這個(gè)過程一般會充滿修改然后重啟的重復(fù)性機(jī)械勞作,偶爾會遭遇無論怎么修改就是不生效、甚至遍尋google也一無所獲的挫折,但是我就是無法厭倦它。
人天生好奇,探索未知事物本身就充滿了樂趣,而且一旦配置奏效,便能獲得滿滿的成就感。新事物對程序員具有極大的吸引力,但是程序員不會止步于使用新事物,而且會在驚奇之余,渴望控制那股背后主導(dǎo)它的力量本身,行使“上帝之力”。
話說回來,為什么我會從vim黨搖身一變成為emacs黨呢?這就不得不提起Clojure這門lisp方言,出于對lisp和函數(shù)式編程的癡迷,我選擇了基于JVM的Clojure作為自己的偏好語言,而emacs天生為lisp而生。
有了這個(gè)充足的理由,我開始收集emacs的cheatsheet并打印出來,天天放在手邊翻閱,甚至買了一本英文版的Learning GNU Emacs書籍,只要有機(jī)會就打開emacs開始刷4clojure上的編程題。由于emacs對lisp的親和性,我?guī)缀鯖]花多少時(shí)間就掌握住了常用的操作技巧。
不過,emacs最負(fù)盛名的學(xué)習(xí)曲線確實(shí)讓學(xué)習(xí)者繞過圈子,只要一段時(shí)間不用,就會忘記很多基本操作。另外,為了更好地在emacs中編寫Clojure,還需要cider-mode和clojure-mode的支持,這時(shí)候就不得不編輯init.el文件,本著KISS (keep it simple, stupid)原則,我照著各種插件的說明文檔中,把配置項(xiàng)復(fù)制粘貼到init.el文件當(dāng)中,運(yùn)行起來沒有問題就好。隨著自定義的內(nèi)容變多,init.el文件也急劇膨脹起來。膨脹本來算不上問題,但我是個(gè)比較有操守的程序員,臃腫的代碼是我極力避免的壞味道(bad smell)。
所以胸臆之中涌動一股浩然之氣,決心學(xué)起emacs lisp,把emacs的配置從頭來過。
從『頭』開始
init.el文件位于~/.emacs.d目錄之下,如果沒有,自行創(chuàng)建一份即可。
首先,我們需要用到emacs的包管理工具package.el,因?yàn)閑macs 24及其以上的版本都已經(jīng)內(nèi)置,所以無需下載到本地,直接通過require加載到emacs的運(yùn)行時(shí)。
(require 'package)
(setq package-archives '(("melpa" . "http://melpa.org/packages/")
("melpa-stable" . "https://stable.melpa.org/packages/")
("marmalade" . "http://marmalade-repo.org/packages/")
("elpy" . "http://jorgenschaefer.github.io/packages/")
("gnu" . "http://elpa.gnu.org/packages/"))
package-enable-at-startup nil)
上面的代碼涉及到setq(變量賦值)的操作,package-archives,顧名思義,多個(gè)包的下載源,我給package-archives設(shè)置了5個(gè)包源,它們之間服從順序的優(yōu)先級,即先從第一個(gè)源中下載包,如果沒有,到第二個(gè)源中尋找,以此類推。
此外,這里("melpa" . "http://melpa.org/packages/")中的點(diǎn)號(dot)表示法也比較奇怪,其實(shí)這是lisp中的Dotted pair表示法,用法和普通的列表類似,但因?yàn)槭?em>pair的緣故,你可以使用(car )獲取"melpa",(cdr )獲取到的卻不再是一個(gè)列表,而是"http://melpa.org/packages/"這個(gè)值本身。
由于國情緣故,所以我推薦使用清華大學(xué)的ELPA鏡像源。
(setq package-archives '(("gnu" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
("melpa-stable" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa-stable/")
("melpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/"))
package-enable-at-startup nil)
熟悉lisp語法
對emacs lisp不熟悉不要緊,先找個(gè)教程練習(xí)一下它的用法,比如learnxinyminutes就非常不錯(cuò)。完成這個(gè)教程,大體不會對elisp犯怵了。接下來,只需要使用c-h v和c-h f查看elisp中定義的變量函數(shù)就能很快上手自行配置。
來個(gè)實(shí)際的例子,在大牛的配置文件中,經(jīng)常能看到如下成對的配置:
(setq package-enable-at-startup nil)
(package-initialize)
開始我覺得這是一對矛盾的配置,package-enable-at-startup設(shè)置為nil,暗示emacs啟動時(shí)不會啟用package,而package-initialize明顯表明在做package的初始化工作。這種時(shí)候,我心中就蹦跶出一句話“世界上本沒有矛盾,如果出現(xiàn)了,檢查你都有哪些前提條件,就會發(fā)現(xiàn)其中一個(gè)是錯(cuò)的”。這種非異常的知識點(diǎn)很難通過搜索引擎找到滿意的答案,而閱讀文檔恰恰是最合適的解決方式。emacs對elisp文檔的支持非常全面,只需將鼠標(biāo)移到package-enable-at-startup變量上,按下c-h v (control + h, v) 組合鍵,就能在其它窗口(window) 看到文檔描述:
Whether to activate installed packages when Emacs starts.
If non-nil, packages are activated after reading the init file
and beforeafter-init-hook'. Activation is not done ifuser-init-file' is nil (e.g. Emacs was started with "-q").
意思是在讀入init.el之后,這個(gè)變量才會生效。換句話說,在讀取init.el的過程中,該變量不論是nil或是non-nil都不會影響package的加載和初始化。所以,這兩者之間并沒有矛盾。當(dāng)然,此時(shí)你可能會想把package-enable-at-startup設(shè)置為nil意欲何為?官方文檔中有如下的解釋:
This will automatically set package-enable-at-startup to nil, to avoid loading the packages again after processing the init file.
簡單點(diǎn)說,就是防止在package-initialize之后重復(fù)加載包,因?yàn)榭赡軙绊懶阅堋?/p>
模塊化
如果把什么東西都揉到init.el文件中,這個(gè)文件一定會很快變得臃腫不堪。為了解決這個(gè)問題,需要引入模塊化的思想——把特定功能的配置放到獨(dú)立的文件中,然后require進(jìn)來。按照慣例,我在~/.emacs.d目錄下建立一個(gè)lisp目錄用于存放所有自定義的模塊文件,隨后在init.el中加入下面這句代碼,意在把lisp目錄加到emacs的加載路徑列表里。
(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))
看似,接下來就可以在每個(gè)獨(dú)立的模塊文件中編寫各種功能的配置。但是由于package.el功能的局限,我們很快就會遇到包重復(fù)安裝和配置漂移(configuration drift)的麻煩。package.el提供了package-install-p(p是predicate的意思)和package-install兩個(gè)配套使用的函數(shù),也就是說一般得先判斷包在不在,才決定安不安裝。幸運(yùn)的是,有人已經(jīng)很好地解決了這部分問題,use-package就是非常好用的包,它將包的配置和包的定義聚合到了一塊,并且保證包一定會安裝在你的系統(tǒng)當(dāng)中。
在使用use-package之前,我們需要先安裝它,如下:
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(eval-when-compile
(require 'use-package))
由于use-package本身就是一個(gè)包,所以可以使用package-install安裝到本地,然后require到emacs的運(yùn)行時(shí),值得一提的是這個(gè)eval-when-compile函數(shù),使用c-h f查看它的定義:
Like ‘progn’, but evaluates the body at compile time if you're compiling.
Thus, the result of the body appears to the compiler as a quoted constant.
In interpreted code, this is entirely equivalent to `progn'.
初次看到compile time,心中難免會有疑問:lisp不是動態(tài)語言嗎,怎么還需要編譯?這種時(shí)候,我們就要求助于elisp的文檔了。在emacs中按下c-h i獲取主話題(topic)的菜單,然后點(diǎn)擊Elisp進(jìn)入它的操作指南。重點(diǎn)查看Evaluation和Byte Compilation兩個(gè)章節(jié)。不難發(fā)現(xiàn)lisp的解析器可以讀取解析兩種類型的lisp代碼,一種是適合人類閱讀的代碼,以el作為后綴;另一種是編譯字節(jié)碼,以elc作為后綴。編譯字節(jié)碼運(yùn)行速度優(yōu)于前一種代碼,我們可以通過byte-compile-file把前一種代碼的文件編譯成字節(jié)碼文件。有趣的是,如果我們使用package來安裝包,對應(yīng)包的目錄下都存在配套的el和elc兩類文件。
在Byte Compilation條目下,有eval-when-compile的完整描述:
If you’re using another package, but only need macros from it (the byte compiler will expand those), then ‘eval-when-compile’ can be used to load it for compiling, but not executing. For example,
(eval-when-compile
(require 'my-macro-package))
這里頭有三個(gè)關(guān)鍵字load、compiling和executing值得留意一下。為了弄懂它們的含義,我們需要了解lisp解析器基本的工作原理:code text -[characters]-> load -[lisp object]-> evaluation/compiling -[bytecode]-> lisp interpretor。換句話說,除非你想編譯包含上述代碼的文件,否則它的作用和progn一模一樣,順序地求值包含其中的表達(dá)式。當(dāng)你正在編譯文件的時(shí)候,包中宏就會原地展開,然后被eval-when-compile宏加載進(jìn)內(nèi)存并被編譯成字節(jié)碼,供后續(xù)解析器執(zhí)行。
Clojure相關(guān)
載入use-package之后,我需要開始配置自己強(qiáng)大的Clojure開發(fā)環(huán)境了。首先,引入幾個(gè)包:
(use-package rainbow-delimiters
:ensure t)
(use-package clj-refactor
:ensure t)
(use-package company
:ensure t
:defer t
:config (global-company-mode))
rainbow-delimiters能夠讓括號變得如同彩虹一樣絢麗(主要是易于區(qū)分forms),clj-refactor是重構(gòu)Clojure程序的神器,company提供了強(qiáng)大的命令補(bǔ)全提示功能。
clojure mode
接下來,我們在~/.emacs.d/lisp目錄下新建一個(gè)init-clojure.el文件,內(nèi)容如下:
(require 'clj-refactor)
(require 'rainbow-delimiters)
(use-package midje-mode
:ensure t)
(defun my-clj-refactor-mode-hook ()
(clj-refactor-mode 1)
(yas-minor-mode 1) ; for adding require/use/import
(cljr-add-keybindings-with-prefix "C-c C-m"))
(use-package clojure-mode
:ensure t
:config
(add-hook 'clojure-mode-hook #'rainbow-delimiters-mode)
(add-hook 'clojure-mode-hook #'subword-mode)
(add-hook 'clojure-mode-hook #'midje-mode)
(add-hook 'clojure-mode-hook #'my-clj-refactor-mode-hook)
(add-hook 'clojure-mode-hook #'enable-paredit-mode))
(provide 'init-clojure)
這里就能看出use-package的好處來了,針對clojure-mode的配置項(xiàng)都統(tǒng)一放到:config中管理起來。配置完畢后,使用(provide 'init-clojure)將模塊以這樣的名字暴露給其它客戶端調(diào)用。
CIDER mode
有了clojure-mode之后,我們還需要一個(gè)Clojure可交互式的開發(fā)工具,CIDER便是這么一款工具。同樣地,我們在lisp目錄下新建一個(gè)名為init-clojure-cider.el,內(nèi)容如下:
(require 'init-clojure)
(require 'company)
(use-package cider
:ensure t
:config
(setq nrepl-popup-stacktraces nil)
(add-hook 'cider-mode-hook 'eldoc-mode)
(add-hook 'cider-mode-hook #'rainbow-delimiters-mode)
;; Replace return key with newline-and-indent when in cider mode.
(add-hook 'cider-mode-hook '(lambda () (local-set-key (kbd "RET") 'newline-and-indent)))
(add-hook 'cider-mode-hook #'company-mode)
(add-hook 'cider-repl-mode-hook 'subword-mode)
(add-hook 'cider-repl-mode-hook 'paredit-mode)
(add-hook 'cider-repl-mode-hook #'company-mode)
(add-hook 'cider-repl-mode-hook #'rainbow-delimiters-mode))
(provide 'init-clojure-cider)
配置的首部,我使用(require 'init-clojure)先加載init-clojure,然后對CIDER本身進(jìn)行一系列的配置。配置的詳細(xì)信息可以通過CIDER github主頁獲取到,這里我就不再贅述。
最后,需要在init.el文件中添加入這么一句(require 'init-clojure-cider),重新啟動emacs,找到一個(gè)Clojure項(xiàng)目,按下C-c M-j (hack-jack-in),就能獲得一個(gè)Clojure的交互式開發(fā)環(huán)境。
小結(jié)
當(dāng)然,我的emacs配置絕對不止這些,但是其余的過程大體類似。由于emacs速來有偽裝成編輯器的操作系統(tǒng)的稱號,所以我的探索是無止境的。如果大家對我的配置感興趣,可以直接去我github上dotfiles上查看。
—
參考鏈接
[1] sriramkswamy dotemacs
[2] purcell emacs.d