=> 上一篇:Emacs:我曾為你留下退路……
前言
我們總是習慣於説,用 Emacs 編輯某文件。對於其他文本編輯器,我們也是習慣如是説。實際上用文本編輯器編輯文件,只是一種假象。我們總是在內存裏編輯着文件的虛相,並在某個時機將虛相寫入文件,從而變為實相。這個事實,倘若你從未用 C/C++ 之類的語言寫過程序或者對計算機運作原理近乎一無所知,通常難以體會。一些能夠自動保存文件內容的編輯器——Emacs 也可以如此——,很容易給你造成錯覺,以為你正在編輯硬盤裏文件的內容。Emacs 極為擅長營造虛相,可謂文字編輯行當的 PhotoShop。
緩衝區
每次當你運行 Emacs 時,即使你只用 Emacs 打開了一份文件,例如 foo.txt,Emacs 會在內存空間創建名為 foo.txt 的緩衝區並將 foo.txt 現有內容複製到該緩衝區,除此之外,Emacs 還有一些緩衝區隱藏在幕後。
執行 M-x list-buffers,你可以看到 Emacs 當前的所有緩衝區的名字,其中有些緩衝區關聯着硬盤上的文件,例如上述的 foo.txt 緩衝區,也有一些緩衝區並不關聯任何文件,例如 *scratch*、*Messages* 以及*Completions 等緩衝區,還有一個緩衝區,甚至不會顯示在 list-buffers 的結果中,它就是微緩衝區。需要注意的是,*Messages* 就是 message 函數用於輸出信息的緩衝區。
在當前窗口裏,可以使用 C-x b 切換緩衝區,倘若你不知道要切換的緩衝區的名字,可以 C-x b TAB,開啓 *Completions* 緩衝區及其窗口,然後從中選擇你要切換的緩衝區。TAB 即製表鍵,亦即 Tab 鍵。在 Emacs 裏以及 Unix 或 Linux 的 Shell 裏,Tab 鍵通常用於自動補全命令,故而在 Emacs 裏,通過它,能開啓 *Completions* 緩衝區及其窗口。
倘若你只是想試驗一下,並不想切換緩衝區,只需將光標定位到 *Completions* 緩衝區所在窗口,摁 q 鍵退出該窗口,然後使用 C-g 終止 C-x b 操作。
使用 C-k 緩衝區名 可以關閉指定的緩衝區,也可以用 C-k TAB 補全的方式,從打開的 *Completions* 緩衝區所在窗口選擇要關閉的緩衝區。
創建緩衝區
在 Elisp 語言中,緩衝區是一種數據類型,通過默認綁定於在 C-x C-f 的 find-file 函數便可為該類型創建實例或對象。例如,表達式 (find-file "/tmp/foo.c") 與 C-x C-f /tmp/foo.c 等效。再例如
(let ((foo-c-buf (find-file "/tmp/foo.c")))
(message "此刻我在緩衝區 %s 裏" (buffer-name foo-c-buf)))
上述代碼是通過 let 表達式,將 find-file 創建的緩衝區對象記為 foo-c-buf,然後用函數 buffer-name 獲取 foo-c-buf 所記緩衝區對象的名字,最後用 message 表達式輸出該名字。在 Emacs 裏對上述表達式求值,即在其尾部執行 C-x C-e,所產生的效應是,Emacs 會創建名為 foo.c 的緩衝區,並將其設置為當前緩衝區,然後在微緩衝區中顯示了該緩衝區的名字。
練習:還記得在「你會寫 Emacs 命令嗎?」中所寫的 c-header 函數嗎?請使用 buffer-name,消除 c-header 的參數。
使用更為基礎的 generate-new-buffer 可創建無文件綁定的緩衝區對象。例如
(let ((foo (generate-new-buffer "Foo")))
(message "緩衝區 %s" (buffer-name foo)))
上述表達式可以創建名為 Foo 的緩衝區對象,並將其記為局部變量 foo,然後在微緩衝區(或 *Message* 緩衝區)輸出該緩衝區對象的名字 Foo。
將創建的緩衝區對象記為變量,是為了後續通過變量訪問該對象,為了便於敍述,我們對二者不作區分,例如一旦將某個緩衝區對象記為某變量,就相當於一個人有了名字,我們可以用此名指代此人。名字不過是符號,用於指代某物。事實上,Elisp 語言裏,變量名的確只是符號,為一個變量賦值的本質是用一個符號綁定一個值。
當前緩衝區
Emacs 運行期間,可以創建多個緩衝區。這些緩衝區可以在某個窗口中呈現,也可以隱藏在幕後,但無論何時,只有一個緩衝區可作為當前緩衝區。可以使用 set-buffer 函數將某個緩衝區對象設為當前緩衝區,例如
(let ((foo (generate-new-buffer "Foo")))
(set-buffer foo))
上述表達式創建了緩衝區對象 foo 並將其設為當前緩衝區。
倘若你希望緩衝區的內容能永久保存下來,在將緩衝區對象設為當前緩衝區後,可使用 set-visited-file-name 將其關聯指定的文件,例如
(let ((foo (generate-new-buffer "Foo")))
(set-buffer foo)
(set-visited-file-name "/tmp/foo.txt")
(message "緩衝區 %s" (buffer-name foo)))
當緩衝區對象 foo 與文件 /tmp/foo.txt 關聯後,緩衝區對象的名字便會由 Foo 變為 foo.txt。使用 with-current-buffer 可以將某個緩衝區對象變為當前緩衝區,並關聯一組相關的操作。例如,上述代碼可等效修改為
(let ((foo (generate-new-buffer "Foo")))
(with-current-buffer foo
(set-visited-file-name "/tmp/foo.txt")
(message "緩衝區 %s" (buffer-name foo))))
使用 with-current-buffer 表達式的好處是,你無需時刻記住當前緩衝區是誰。
內容轉移
緩衝區,也許是 Elisp 語言裏最為重要的數據類型了,其功用令其他編程語言所提供的字符串類型難以企及。在當前緩衝區內,你可以移動光標,在任意位置插入文字,可以隨時將緩衝區內容保存到文件,也可以將一個緩衝區中的內容複製到另一個緩衝區。
insert-buffer 可將指定的緩衝區的內容插入到當前緩衝區光標所在位置。例如
(let ((foo (generate-new-buffer "Foo"))
(bar (generate-new-buffer "Bar")))
(with-current-buffer foo
(insert "i am foo!"))
(with-current-buffer bar
(insert-buffer foo)))
上述代碼創建了兩個緩衝區對象 foo 和 bar。在 foo 中,我們隨意插入了文字「i am foo!」。然後,我們將 foo 中的內容插入到了 bar 中。
不過,insert-buffer 是可交互函數,亦即它需要通過 M-x 的方式調用,在 Elisp 程序裏直接調用它,Emacs 不太同意這種行為。正確的方式是使用 insert-buffer 的非可交互版本,即 insert-buffer-substring,後者可以將給定的緩衝區及其內容的子域插入到當前緩衝區。例如
;; 將 foo 緩衝區全部內容插入到當前緩衝區
(insert-buffer-substring foo)
;; 將 foo 緩衝區的第 1 至第 7 個字符插入到當前緩衝區
(insert-buffer-substring foo 1 7)
erase-buffer 可以清除當前緩衝區的內容,若在上述示例中使用這個函數,實現的效果便是,foo 中的內容被轉移到了 bar。
(let ((foo (generate-new-buffer "Foo"))
(bar (generate-new-buffer "Bar")))
(with-current-buffer foo
(insert "i am foo!"))
(with-current-buffer bar
(insert-buffer-substring foo)
(with-current-buffer foo
(erase-buffer))))
函數 point-min 和 point-max 可分別獲取當前緩衝區的開始和末尾的位置。例如,若當前緩衝區一共含有 100 個字符,則 (point-min) 的求值結果為 1,而 (point-max) 的結果為 100,藉助前者,我們可以將光標移動到緩衝區首部,藉助後者不僅可將光標移動到緩衝區尾部,而且也能獲知緩衝區的長度。函數 point 可以獲取當前緩衝區中,光標的當前位置。當前緩衝區的光標可以通過 goto-char、forward-char 以及 backward-char 移動。想必你還記得我們曾經用過它們。
基於上述的緩衝區內的光標定位功能,可以自由地將一個緩衝區中的內容插入到另一個緩衝區中指定的位置,也許你此刻還不理解有何必要如此。很快,我會向你證明它的意義。
賦以窗口
當前的緩衝區未必在當前的窗口裏呈現。不過,當輸入焦點落入某窗口,亦即該窗口被激活,則與該窗口關聯的緩衝區會自動成為當前緩衝區。
selected-window 可獲得當前被激活的窗口。例如
(let ((live-window (selected-window)))
(when (windowp live-window)
(message "這是一個窗口"))
(when (window-live-p live-window)
(message "這是一個激活的窗口")))
windowp 和 window-live-p 皆為 Elisp 謂詞函數,分別用於判定一個對象是否為窗口以及是否為激活的窗口。when 表達式,想必你還記得,它表達「若……為真,則……」的邏輯。
如同總有一個緩衝區對象是當前的,也總有一個窗口是激活的。在激活的窗口裏,可以使用 switch-to-buffer 將指定的緩衝區對象呈現於窗口中並使之成為當前緩衝區。例如
(let ((foo (generate-new-buffer "Foo")))
(switch-to-buffer foo)
(set-visited-file-name "/tmp/foo.txt"))
上述表達式與 (find-file "/tmp/foo.txt") 等效。
也許你還記得 C-x 2 和 C-x 3,它們可以構造新窗口,它們分別綁定函數 split-window-right 和 split-window-below。這意味着,在 Elisp 程序裏,可以利用這兩個函數創建新窗口,並使用 select-window 函數(注意,它不是 selected-widow 函數)將其激活。例如
(let ((new-window (split-window-below)))
(select-window new-window))
對上述表達式求值,產生的效果是,當前窗口從正中間被橫向一分為二,上方是原有窗口,下方是新建窗口,且輸入焦點落在下方窗口裏。
結合上述緩衝區的函數,我們可以讓創建新窗口的示例更為生動。例如,創立緩衝區,將其關聯到某個文件,然後為該緩衝區新建窗口。
(let ((foo (generate-new-buffer "Foo"))
(foo-win (split-window-below)))
(select-window foo-win)
(switch-to-buffer foo)
(set-visited-file-name "/tmp/foo.txt"))
總結
我們已經步入 Emacs 世界裏我認為最為有趣的地方。在此處,你能直觀深切感受到,天地之間,其猶橐龠乎?虛而不屈,動而愈出。你在 Emacs 窗口中的一切所見,不過是幕後的緩衝區在各種命令揉捏雕琢的形狀在某個時間剖面上的投影。人類對 Emacs 可作出的配置,是不可窮盡的,但緩衝區機制是非常容易把握的,故曰多聞數窮,不若守中。
=> 下一篇:在 Emacs 緩衝區裏行走的姿勢