首发于Ghost in 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 " \t")
(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,它的默认值是 ” \t”,即空格加 Tab ,之所以要额外定义它,主要是为了方便在不同的 Major-Mode 下添加新的符号,例如在 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)))