你能想到的幾乎所有關於行的操作

這一期我們專門討論行操作。這裡所謂的「行操作」,是指所有跟行有關的操作,在一般的編輯器里,常見的行操作有移動到行首/行尾,上下移動,複製/選中/剪切/注釋整行,交換兩行;如果算上段落操作的話,類似的就還有移動到段首/段尾,複製/選中/剪切/注釋段落,交換兩段——我能想到的差不多就這麼多。

但是在 Emacs 里,你能做的遠遠不止這些。下面就來分享一下我目前為止自己寫的所有相關命令。這裡邊需要用到的內建函數大概有十來二十多個,另外也有一些我自己造的新輪子。

一般來講,一行代碼有三個關鍵的位置點,一是行首,二是從行首出發跳過縮進,也就是該行的第一個字元,三是行尾。我寫了兩個命令,可以控制游標在這三個點之間循環切換。切換的順序分別是

  • c-move-forward-line: bol -> skip-bol -> eol -> bol
  • c-move-backward-line: eol -> skip-bol ->bol ->eol

這裡的前綴 "c-" 代表這是一個 interactive function 也就是 command ,bol 和 eol 分別代表 beginning-of-line 和 end-of-line 。這兩個命令的具體定義如下:

(defun c-move-forward-line () (interactive) (if (eq major-mode org-mode) (cond ((eolp) (f-skip-bol) (setq -move 1)) (t (end-of-line) (setq -move 2))) (cond ((and (eolp) (not (bolp))) (beginning-of-line) (setq -move 0)) ((>= (current-column) (f-skip-bol t)) (end-of-line) (setq -move 2)) (t (f-skip-bol) (setq -move 1)))))(defun c-move-backward-line () (interactive) (let ((col (f-skip-bol t))) (if (eq major-mode org-mode) (cond ((and (<= (current-column) col) (not (= col 2))) (org-up-element) (skip-chars-forward -chars) (setq -move 1)) (t (f-skip-bol) (setq -move 1))) (cond ((and (bolp) (not (eolp))) (end-of-line) (setq -move 2)) ((<= (current-column) col) (beginning-of-line) (setq -move 0)) (t (f-skip-bol) (setq -move 1))))))(defvar -chars " ")(make-variable-buffer-local -chars)(defvar -move 0)(make-variable-buffer-local -move)

大體的思路就是先獲取當前的游標,判斷其處於哪個位置,然後移動到下一個指定位置:bol、skip-bol 或者 eol。命令里對 org-mode 下的行為做了特殊規定,具體效果可自行試驗。這裡要著重講一下 f-skip-bol 這個輪子,它是用來判斷當前游標是否位於 skip-bol 以及移動到此處的函數。其定義如下:

(defun f-skip-bol (&optional save) (let ((col (save-excursion (beginning-of-line) (skip-chars-forward -chars) (current-column)))) (unless save (move-to-column col)) col))

可以看到它的作用就是讓游標移動到 skip-bol 處,如果可選參數 non-nil 的話,則不移動游標,只是單純返回 skip-bol 的列數。這裡還有一個重要的變數是 -chars,它的默認值是 " ",即空格加 Tab ,之所以要額外定義它,主要是為了方便在不同的 Major-Mode 下添加新的符號,例如在 org-mode 里跳過標題欄開頭的*號。

(setq -chars (concat "*" -chars)) ; org-mode

有了這幾個東西之後,就可以來優化一下原本的上下方向鍵了:指定游標在上下移動的時候,保持在行首/行尾或者 skip-bol 這三個位置,或者執行正常的移動。指定方式通過 -move 這個變數來實現,其值分別為 bol -> 0, skip-bol -> 1, eol -> 2。於是有:

(defun f-move-up-or-down (n) (unless (minibufferp) (cond ((and (= -move 2) (eolp)) (next-line n) (end-of-line)) ((and (= -move 1) (= (current-column) (f-skip-bol t))) (next-line n) (f-skip-bol)) (t (next-line n) (setq -move 0))) (f-visual-mode)))(defun c-move-down () (interactive) (f-move-up-or-down 1))(defun c-move-up () (interactive) (f-move-up-or-down -1))

通過檢測 -move 以及當前位置來判斷是否需要在上下移動時鎖定 bol/skip-bol/eol。函數最後的 f-visual-mode 是指在執行這樣的操作之後觸發 visual-mode (見上一期文章,你可以自行定義它的觸發條件),之後無論是複製還是幹嘛就都可以單鍵操作了。

如果是對於游標在段落間的移動,事情就要簡單很多,代碼如下:

(defun c-paragraph-backward () (interactive) (unless (minibufferp) (if (not (eq major-mode org-mode)) (backward-paragraph) (org-backward-element) (skip-chars-forward -chars)) (f-visual-mode)))(defun c-paragraph-forward () (interactive) (unless (minibufferp) (if (not (eq major-mode org-mode)) (forward-paragraph) (org-forward-element) (skip-chars-forward -chars)) (f-visual-mode)))

同樣的,這裡對 org-mode 做了特殊的修飾,並選擇在移動結束後觸發 visual-mode。這可以說是一個非常貼心的設定,因為通常情況下,編輯狀態往往對應極小範圍的移動,而對於諸如段落這樣的大範圍的移動,往往伴隨的是複製粘貼,另起一行或者退回上一行這樣的非輸入操作,這時使用 visual-mode 簡直再合適不過了。

除游標移動以外,交換兩行/兩段落的也是非常常見的需求,但在一般的編輯器包括 Emacs 里,交換兩行之後不會有游標跟隨,這樣的壞處是你無法實現連續操作(例如把原本第1行的代碼,往下一直挪挪挪,插到原本的第4、5行之間)。而對於段落移動,Emacs 所提供的函數同樣沒有游標跟隨,且在交換第1、2段時由於第1段前沒有空行而導致 Bug。所以這裡我特地重寫了這四個函數:

(defun c-transpose-lines-down () (interactive) (unless (minibufferp) (delete-trailing-whitespace) (end-of-line) (unless (eobp) (forward-line) (unless (eobp) (transpose-lines 1) (forward-line -1) (end-of-line)))))(defun c-transpose-lines-up () (interactive) (unless (minibufferp) (delete-trailing-whitespace) (beginning-of-line) (unless (or (bobp) (eobp)) (forward-line) (transpose-lines -1) (beginning-of-line -1)) (skip-chars-forward -chars)))(defun c-transpose-paragraphs-down () (interactive) (unless (minibufferp) (let ((p nil)) (delete-trailing-whitespace) (backward-paragraph) (when (bobp) (setq p t) (newline)) (forward-paragraph) (unless (eobp) (transpose-paragraphs 1)) (when p (save-excursion (goto-char (point-min)) (kill-line))))))(defun c-transpose-paragraphs-up () (interactive) (unless (or (minibufferp) (save-excursion (backward-paragraph) (bobp))) (let ((p nil)) (delete-trailing-whitespace) (backward-paragraph 2) (when (bobp) (setq p t) (newline)) (forward-paragraph 2) (transpose-paragraphs -1) (backward-paragraph) (when p (save-excursion (goto-char (point-min)) (kill-line))))))

這四個函數的代碼都有點長,主要是把各種邊界條件(如文件頭、文件尾,首行非空、trailing-whitespace)都給考慮進去了,把它們拷到你的配置文件里試一下,你會發現這四個交換內容的函數簡直貼心好用到爆!

最後再貼幾組將源碼優化過後的常見函數,代碼雖然簡單,但同樣貼心實用,大概看一下函數名你就知道是怎麼回事。我就不贅述了。

(defun c-copy-buffer () (interactive) (save-excursion (goto-char (point-max)) (unless (or (eobp) buffer-read-only) (newline))) (delete-trailing-whitespace) (kill-ring-save (point-min) (point-max)) (unless (minibufferp) (message "Current buffer copied")))(defun c-indent-paragraph () (interactive) (save-excursion (mark-paragraph) (indent-region (region-beginning) (region-end))))(defun c-kill-region () (interactive) (if (use-region-p) (kill-region (region-beginning) (region-end)) (kill-whole-line) (back-to-indentation)))(defun c-kill-ring-save () (interactive) (if (use-region-p) (kill-ring-save (region-beginning) (region-end)) (save-excursion (f-skip-bol) (kill-ring-save (point) (line-end-position))) (unless (minibufferp) (message "Current line copied"))))(defun c-set-or-exchange-mark (arg) (interactive "P") (if (use-region-p) (exchange-point-and-mark) (set-mark-command arg)))(defun c-toggle-comment (beg end) (interactive (if (use-region-p) (list (region-beginning) (region-end)) (list (line-beginning-position) (line-beginning-position 2)))) (unless (minibufferp) (comment-or-uncomment-region beg end)))

這一期算是大出血了,掏出了不少我自己辛苦調教多年的壓箱底的配置。下一期講點輕鬆的內容:Emacs 的配置文件架構,即如何將 init.el 的配置代碼分散到多個角色不同的文件中去,同時實現自動化的包管理以及多終端同步。

推薦閱讀:

為什麼維基百科的編輯器不是所見即所得的?
VSCode、Atom和Sublime為什麼不支持代碼標籤成對摺疊呢?
vim/gvim 有哪些實用技巧?
Markdown初識
Sublime 自動縮進怎麼設置?

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