博客 / 詳情

返回

Emacs 第三方包,不祥之器……

上一篇:Emacs 是他們的,也是我的

前言

Emacs 有大量的第三方軟件包,這些包大多數是用 Elisp 語言編寫,也有少量用 C 語言編寫。Emacs 可以通過 init.el 載入它們,以增強自身功能。這些軟件包猶如一支強大的軍隊,你可以調動它們去征服一切文檔。不過,在試圖學習如何調動軍隊之前,先接受老子潑的一盆冷水:兵者,不祥之器,非君子之器,不得已而用之,恬淡為上。

Markdown

假如你需要用 Emacs 編輯 Markdown 格式的文檔,如同此刻我之所為,你可以為 Emacs 安裝名為 markdown-mode 的包,它能讓 Emacs 對 Markdown 語法產生高亮效果,也提供了一些便捷命令,讓你在編寫 Markdown 文檔是更有效率。使用以下命令

M-x package-install RET markdown-mode RET

便可安裝 markdown-mode 包,前提是你的機器已聯網且可以訪問以下網址:

  • https://elpa.gnu.org/packages/
  • https://elpa.nongnu.org/nongnu/

上述網址即 Emacs 內置的軟件源,即軟件倉庫。elpa 即 elisp package 的簡寫。

一旦 Emacs 從軟件源安裝成功了某個包,它會向 init.el 的尾部自動寫入一些配置。例如 markdown-mode 包安裝後,init.el 的尾部會自動出現以下內容:

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(package-selected-packages '(markdown-mode)))
(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 )

這部分內容 Emacs 會自己管理,你無需去改動它。去掉註釋,上述代碼最為關鍵之處在於

(custom-set-variables
 '(package-selected-packages '(markdown-mode)))

這與我們之前所接觸的 Elisp 語法有些區別,你只需要知道 package-selected-packages 是列表類型的變量,用於記錄我們通過 package-install 從 Emacs 軟件源中安裝的所有包。

之後,每次你用 Emacs 編輯 Markdown 格式的文檔時,只要文檔對應的文件名以 .md 為後綴,Emacs 便會自動開啓 Markdown 模式。

Markdown 模式

字節碼文件

我們安裝的 markdown-mode 包,默認存放於 ~/.emacs.d/elpa 目錄,倘若你對其實現有些好奇,可進入包目錄,查看其源碼文件,即 markdown-mode.el,同一目錄下還有同名的 .elc 文件,即 markdown-mode.elc,它是 Emacs 編譯 markdown-mode.el 文件生成的字節碼文件。當我們在 Emacs 裏啓用 markdown 模式時,Emacs 會自動加載 markdown-mode.elc,而非 markdown-mode.el,原因是前者的載入和執行速度都比後者更快。

Emacs 有兩種執行 Elisp 程序的方式,一種是對 .el 文件中的全部表達式逐行讀取,並在該過程中,對所得表達式解釋和求值,這是我們已經非常熟悉的方式,因為我們也經常用 C-x C-e 對一些表達式求值。只是之前,我們察覺不到 Emacs 對錶達式解釋的過程。

所謂的,Emacs 對錶達式的解釋過程,你可以理解為,當你看到 (+ 1 2) 的時候,若你不懂 Elisp 語言,你心裏想的是,這是一個左括號,一個加號,一個空格,數字 1,又一個空格,數字 2 以及一個右括號構成的一小段文字,而當你已經懂得 Elisp 語言時,你心裏想的則是,這是一個表達式,在計算 1 + 2。你心裏想的東西,從不懂 Elisp 語言時的狀態到你懂了 Elisp 語言的狀態,這便是對 Elisp 表達式解釋的過程。

我們用 Emacs 將一份 .el 文件編譯為 .elc 文件時,這個編譯過程,便是對 .el 文件裏所有表達式的解釋過程,而 .elc 文件存放的便是解釋結果。Emacs 載入 .elc 文件,便是第二種執行 Elisp 程序的方式,這種方式之所比第一種要快,是因為在開始載入 .elc 文件的那個瞬間就意味着對 Elisp 的程序的解釋已經完成,剩下的只是對 Elisp 程序裏的一個又一個表達式的求值過程。

還記得嗎?我們曾將 init.el 裏我們最為熟悉的那部分配置轉移到 ~/.my-emacs 目錄裏的 my-config.el。現在,你可以做一個試驗。用 Emacs 打開 my-config.el 文件,然後執行 M-x byte-compile-file RET RET,便可在 ~/.my-emacs 目錄得到 my-config.elc 文件。然後,在 init.el 文件裏,將原來的

(load "my-config.el")

修改為

(load "my-config.elc")

之後每次你在啓動 Emacs 的時候,它便會快一點,大概是幾納秒吧……

特性

現在,在 my-config.el 文件的末尾添加以下代碼:

(provide 'my-config)

然後將 init.el 文件裏的

(load "my-config.elc")

修改為

(require 'my-config)

require 函數的作用與 load 相似,但是它會優先加載 my-config.elc,倘若沒有 my-config.elc,它會加載 my-cofig.el,亦即 require 是聰明的 load,只是需要與 provide 函數配合使用。

provide 的作用是向 Emacs 註冊特性,表示該文件的某些功能已就緒。例如上述示例中的 `'my-config 便是一個特性。一份 .el 文件裏可能含有多個特性。Emacs 在執行 .el 文件中的程序時,遇到 provide 表達式,便認為一項特性已加載完成,但仍會執行後續代碼,只是後續代碼不是特性,或者它們屬於另一個特性。

require 可以加載給定特性對應的代碼。由於 'my-config 特性是在 my-config.el 文件尾部提供的,故而 require 能加載 my-config.el 文件裏的一切內容。

模式關聯

每次編輯 .md 文件時,Emacs 便會自動開啓 Markdown 模式,對此你不覺得詭異嗎?安裝 markdown-mode 包之後,我們在 init.el 裏並未作任何配置。Emacs 是如何將 .md 文件與 Markdown 模式關聯起來的呢?秘密在於 Emacs 的全局變量 auto-mode-alist,其類型為關聯列表。

倘若你打開 markdown-mode 包中的 markdown-mode.el 文件,搜索 auto-mode-alist,應該能找到以下代碼:

;;;###autoload
(add-to-list 'auto-mode-alist
             '("\\.\\(?:md\\|markdown\\|mkd\\|mdown\\|mkdn\\|mdwn\\)\\'" . markdown-mode))

上述代碼將 .md.markdown.mkd 之類的後綴名與 markdown-mode 命令關聯了起來,並將其添加到 auto-mode-alist。函數 add-to-list 可向列表首部添加元素,它與之前我們用過的 conspush 函數的區別是,它在向列表中添加元素之前,會先檢測待添加的元素在列表中是否已經存在,若存在,則放棄添加。

可能有一些包在實現中,並未將其所實現的模式命令與文件擴展名關聯起來,或者他們所作的關聯並不合乎我們的要求,這時便需要我們在 init.el 文件裏用 add-to-listauto-mode-alist 添加我們所定義的模式關聯了。注意,文件的後綴名可以是正則表達式。

軟件包管理

在 Emacs 裏,執行 M-x package-list-packages,可以打開軟件列表。

在軟件列表中,若將光標移動到某一行,執行 i,即單擊 i 鍵,Emacs 便會將該行對應的軟件包標記為待安裝狀態,在 Emacs 界面左側邊欄會顯示 I 字樣。你可以用 i 鍵將多個軟件包標記為 I 狀態。然後執行 x,Emacs 便會安裝所有被標記為 I 的軟件包。

執行 M-x package-refresh-contents,可以從 Emacs 軟件源獲取最新的軟件信息。該操作完成後,在軟件列表中,執行 r,便可刷新軟件列表。

每次更新完軟件信息後,你可以執行 U,即 Shift + u 或將鍵盤切換為大寫模式,單擊 U,該操作會在軟件列表中將你之前安裝的包標記為更新,然後執行 x,Emacs 便會為你更新軟件包。

將光標移動到你之前安裝的軟件包所在的行,執行 d,Emacs 便可將該行軟件包標記為 D 即待刪除狀態。然後執行 x,Emacs 便會刪除所有被標記為待刪除的包。

增加軟件源

除了 Emacs 內置的兩個軟件源之外,也有一些第三方軟件源,其中翹楚是 melpa 源。與 Emacs 內置的軟件源相比,melpa 源更為激進,軟件包的版本通常較新。倘若你覺得,更新的,是更好的,可以在 init.el 中,將 melpa 源添加到 Emacs 軟件源列表裏,例如

(use-package package
  :config
  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
  (unless (bound-and-true-p package--initialized)
    (package-initialize)))

上述代碼,你應該能看懂第三行,即 add-to-list 表達式。

unless 表達式,是我們之前沒有學過的,它表達的邏輯與 when 相反。上述代碼中的 unless 表達式實現的邏輯是,若軟件源尚未初始化,則進行初始化。

bound-and-true-p 用於判斷一個變量是否綁定到了某個值。如果 Emacs 的軟件源已經初始化了,package--initialized 便會綁定到一個值,此時 bound-and-true-p 表達式的求值結果便為真,即 t,於是上述 unless 表達式即

(unless t
  (package-initialize)

上述表達式裏的 (package-initialize) 是不會被求值的。倘若用 when 替換 unless,則 (package-initialize) 便會被求值。於是,unless 表達的邏輯就是,除非條件為真,否則便如何。

上述表達式,若改為 when 實現,需要寫為

(when (not (bound-and-true-p package--initialized))
    (package-initialize))

實際上,上述代碼中的 unless 表達式,除了能幫助你理解 unless 的用法外,並無意義。我們在使用 package-installpackage-list-packages 等命令時,Emacs 會自動初始化軟件源。故而,我們不需要它。在 init.el 文件裏,你只需像下面這樣設定,便可將 melpa 源添加到軟件源列表:

(use-package package
  :config
  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")))

那個 unless 表達式之所以存在,是因為那段代碼是我從別人那裏抄來的。

現在,你唯一不明白的應該是 use-package 了。

軟件包配置

use-package 是 Emacs 內置的軟件包配置命令。實際上,它在 Emacs 29.1 時才被 Emacs 內置。在此之前,它是第三方軟件包 use-packaeg 包裏的命令。Emacs 29.1 將 use-package 包內置,成為了自身的一部分。

use-package 命令誕生之前,在 init.el 裏配置 Emacs 軟件包,基本是每個軟件包都有有其專屬的配置方式。在網上你現在應該隨意便能搜到一些 Emacs 用户共享的 init.el 文件,從這些文件裏應該能看到很多過去的時光。現在,Emacs 軟件源裏的大多數(我猜測的)軟件包,皆可用 use-package 命令統一配置。例如,我們可以對 markdown-mode 包做一些配置,如下

(use-package markdown-mode
  :ensure t
  :mode ("\\.md\\'" . markdown-mode)
  :init 
  (message "開始載入 Markdown 模式")
  :config 
  (message "你可以對 Markdown 模式作一些定製"))

倘若你將上述代碼添加到 init.el 文件裏,之後你每次用 Emacs 打開 .md 文件時,*Messages* 緩衝區便會出現以下信息

開始載入 Markdown 模式
你可以對 Markdown 模式作一些定製

use-package 命令的第一個參數是軟件包的名字,之後的參數皆為鍵值對,鍵的形式都是 :key,值則是 Elisp 表達式。這裏,你需要將其與之前我們接觸過的關聯列表裏的鍵值對有所區分。下面對上述代碼中出現的鍵值對予以説明:

  • :ensure 對應的值為 t,表示所配置的軟件包尚未安裝,則自動從 Emacs 軟件源安裝它,然後再予以配置。
  • :mode 對應的表達式可用於構造模式關聯。對於 markdown-mode 包而言,這個鍵值對是可以省略的,因為前面我們探索過 markdown-mode 包的模式關聯。
  • :init 對應的表達式可在 Emacs 載入軟件包前執行一些初始化工作。
  • :config 對應的表達式可以對軟件包作一些設定,這也是我們使用 use-package 命令的動機。

練習:執行 C-h f use-package RET,閲你之所見。

現在,可以將上述的 use-package 代碼從 init.el 裏刪除,然後添加以下代碼:

(use-package markdown-mode
  :config 
  (set-face-attribute 'markdown-pre-face nil
                      :foreground "darkblue"))

上述代碼對 Markdown 模式裏源碼排版區域的文字顏色由默認的墨綠色修改為深藍色。上述配置生效前後的效果對比如下圖所示:

Markdown 模式配置生效前

Markdown 模式配置生效後

總結

現在,你有了一支強大的軍隊了,甚至有着對它基本的掌控能力。如何運用這支軍隊,除了勿忘老子所言,也可以研讀一些孫子兵法……現在,我們也可以相忘於江湖了。將來惹出禍來,莫要把我説出來。

最後,我想告訴你的是,Elisp 程序還有第三種執行方式。假設你寫了一個 Hello world 程序,即 hello.el:

;; hello.el
(princ "Hello world!\n")

在終端(或命令行窗口)裏執行以下命令

$ emacs -Q --script hello.el

你會看到一個新的世界。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.