那就從妖艷酷炫的快捷鍵開始吧!(一)

Ghost in Emacs, 題目靈感取自於日本動漫 Ghost in the Shell (攻殼機動隊)。在我看來,Emacs 正是一款擁有靈魂的編輯器,其靈活的 Lisp 語言,優雅的S表達式,強大的宏命令,豐富的插件庫,可以說為用戶提供了幾乎無限的自由定製(想像力)空間。

每個人的 Ghost 都是獨一無二的,相信每個真正 Emacs 用戶的配置也是如此。所以本專欄保證,從始至終所寫的每一個函數,實現的每一個宏,都是絕對的原創。部分從源碼中優化改進的函數也會註明。

常見的 Emacs 的快捷鍵設置主要有四種類型,全局快捷鍵,全局映射鍵,基於 Major-Mode 的局部快捷鍵,以及基於 Minor-Mode 的局部快捷鍵,對應的命令分別是

(global-set-key (kbd "A") your-command)n(define-key key-translation-map (kbd "A") (kbd "B"))n(local-set-key (kbd "A") your-command)n(define-key your-minor-mode-map (kbd "A") your-command)n

這裡沒有提到 Spacemacs 的特色也就是 Evil-Mode 。實際上我沒用過它們,並不了解具體是怎麼實現的。不過不用擔心, Emacs 支持用簡短的 Lisp 代碼自定義一個類似於 Vim 的 Visual-Mode,大概就20行左右。輕鬆做到單鍵執行大部分操作,拯救你的小拇指!具體內容放在第二期講。

好了回到正題,如果要刪除或者禁用某個鍵,是這樣

(global/local-unset-key (kbd "A"))n(global/local-set-key (kbd "A") ignore/nil)n

這裡在代碼中所使用的

global/localnignore/niln

代表著兩種或多種不同的方式的並列,請注意。

以上內容很基礎,用過的都明白。但其實絕大部分 Emacs 新手都會碰到的一個頗為棘手的問題是:鍵的衝突。例如你用

global-set-keyn

定義好了你所需要的鍵,那它很可能會在進入 Major-Mode 之後被插件中已定義好的局部鍵給覆蓋了(或者你明明禁用了某個鍵,卻在某個 Major-Mode 里發現它又復活了)。你為了防止這種情況的發生於是用

define-key key-translation-mapn

直接暴力映射,這樣看起來誰也改不了。然而更麻煩的還在後頭,假如你用全局映射的方式使得A鍵變成了B鍵,你在進入某個 Major-Mode 之後A還是牢牢綁在B上頭,但悲劇的是B原本的命令被局部設置給改了,於是A就又變成了不知道從哪兒冒出來的C。

這就是讓新人普遍頭疼的鍵衝突問題,對於鍵空間本就緊湊的原生 Emacs 而言簡直就是一場災難。不過好在解決的辦法其實很簡單:找一個沒怎麼用的 prefix 鍵作為專用的代理鍵,先映射到這個懸空的代理鍵上,然後再全局或者局部設置它。可以看下面的代碼:

(define-key key-translation-map (kbd "A") (kbd "M-g A"))n(global/local-set-key (kbd "M-g A") your-command)n

這樣做的好處是,由於你把A映射到了一個稀有罕見的代理的前綴上頭,所以永遠不用擔心會被局部鍵給覆蓋了。你可能會覺得像這樣每個鍵都得寫兩行代碼很麻煩,那我們來寫個宏好了:

(defmacro m-map-key (key obj)n `(let* ((ks (cadr ,key)) (mk (kbd (concat "M-g " ks))))n (define-key key-translation-mapn ,key (if (symbolp ,obj) (progn (global-set-key mk ,obj) mk) ,obj))))n

在 Emacs 里,宏和函數的主要區別在於,函數的參數是在傳入時 eval ,而宏則是傳入並展開後再 eval 。所以你可以把一個全局變數作為參數傳進宏里,然後重新給它賦值,具體這裡不細講。總之有了上面這個宏以後問題就變得很簡單,你只需寫

(m-map-key your-command (kbd "A"))n

就可以實現先映射到代理鍵再定義的功能。而對於某些容易被覆蓋的快捷鍵而言,用直接映射會比較好,例如 "C-y" 代表的 yank 到了 Org-Mode 里會被替換為 org-yank 。如果你把某個鍵映射到了 "C-y" 上,那它也會隨之變化。對於這種情況,直接寫

(m-map-key (kbd "B") (kbd "A"))n

就可以,相關的判斷邏輯已經寫在上面的宏裡邊了。注意這裡我採用了 Windows 系統 Scancode 這種映射的順序,按A的時候實際執行B。這個宏名字里的前綴 m- 代表著它是一個 macro,同理如果我定義一個函數會用 f- 做前綴,定義一個命令會用 c- 做前綴,定義一個變數會用 - 做前綴。後邊可以陸續看到。

從這個例子中我們可以看出,Emacs 里不同的快捷鍵設置方式是有優先順序區別的,具體來講,優先順序從高到低的順序是:

key-translation-map > minor-mode-map > local-set-key > global-set-keyn

在你按照上述方式設置了代理映射的快捷鍵之後,你便可以在某些 Major-Mode 里很方便的設置局域快捷鍵,例如你希望在 python-mode 里讓原本 eval-last-sexp 的鍵變成運行當前行的 Python 代碼,你可以這樣寫:

(defun f-python-mode ()n (local-set-key (kbd "C-x C-e") f-python-shell-send-line)n (local-set-key (kbd "M-g C-y") f-python-shell-send-line))n(add-hook python-mode-hook f-python-mode)n

這裡 eval-last-sexp 原本的鍵是 "C-x C-e",可以在當前 Mode 下修改它的綁定函數。由於我個人還另外設置了

(m-map-key eval-last-sexp (kbd "C-y"))n

所以我需要在設置局部按鍵時,寫出相應的代理映射鍵即 "M-g C-y"。另外要注意的是,Python-Mode 里並沒有自帶的「運行當前行」的命令,所以我自己寫了一個 f-python-shell-send-line ,這一類的實用小命令我寫過很多很多,在後續的文章中也會陸續講到。為了保證能有盡量長期的乾貨輸出,這一期就先講這麼多。

下一期我會演示怎麼自己寫一個類似於 Evil 的 Visual-Mode,並在本期所講的

m-map-keyn

的基礎上加入更複雜的邏輯,使其可以同時執行 Visual-Mode 快捷鍵設置。


推薦閱讀:

Lisp 能被用來幹什麼?
先學lisp好還是先學haskell好?
對 Lisp 新手來說,學習哪種方言、使用哪些參考書和開發軟體更適合?
SICP換零錢迭代方法實現,是如何寫的?
根據遞推關係,如何編程計算這個數列的前10項?

TAG:Emacs | Lisp | 文本编辑器 |