470
技術社區[雲棲]
我也說說Emacs吧(4) - 光標的移動
在說基本編輯命令之前,我們先加一個小tip,說說如何將函數和鍵綁定在一起。
(define-key global-map [?\C-l] 'recenter-top-bottom)
define-key函數需要三個參數,第一個是綁定表的名稱,不同的模式下的描述表是不同的。第二個參數是鍵名,第三個參數是鍵要綁定的函數名。
移動光標
無模式和有模式概述
emacs是一種無模式的編輯器,這也是除了vi之外大部分編輯器的標準做法。每個輸入的字符都會直接輸入到緩衝區裏。編輯要用到的功能函數,就隻好綁定到組合鍵上,主要是Ctrl鍵,Esc或Alt鍵的組合鍵。
比如,最基本的光標移動。如果有上下左右鍵,就用上下左右鍵。沒有的話,emacs會用C-f向右,C-b向左,C-n向下一行,C-p向上一行。C-a移動到行首,C-e移動到行尾。
大量使用Ctrl和Alt,Esc鍵,使得手需要經常移動,小指被過度使用。
而vi的采用正常模式和編輯模式分離來解決這個問題,在正常模式下,不能輸入字符,所有的字符都被當成命令來執行。此時,j是下一行,k是上一行,h是向左,l向右。效率要比C-n,C-p,C-b,C-f要高。0到行首,$到行尾。
但是vi的問題就是,需要經常在正常模式和編輯模式來回切換。
spacemacs使用evil來模擬vi的這種模式,而且有些鍵的綁定與標準emacs有所不同。
光標左右移動
移動光標是最基本的命令了,這其中最基本的是光標左右移動,和上下移動。
我們先學習emacs的標準方式:
- 向右一個字符: C-f (forward-char)
- 向左一個字符: C-b (backward-char) 這兩個函數都是用C語言實現的,所以沒有lisp源碼,目前我們暫時先關注lisp部分。
但是,在spacemacs的默認情況下,這兩個綁定已經被取消了。因為spacemacs默認是用vi的模式方案,在正常模式下,使用h鍵左移,l鍵右移。
l鍵和右箭頭鍵,綁定到evil-forward-char函數上. 最終,evil-forward-char還是會調用到forward-char來實現移動的功能的:
(evil-define-motion evil-forward-char (count &optional crosslines noerror)
:type exclusive
(interactive "<c>" (list evil-cross-lines
(evil-kbd-macro-suppress-motion-error)))
(cond
(noerror
(condition-case nil
(evil-forward-char count crosslines nil)
(error nil)))
((not crosslines)
;; for efficiency, narrow the buffer to the projected
;; movement before determining the current line
(evil-with-restriction
(point)
(save-excursion
(evil-forward-char (1+ (or count 1)) t t)
(point))
(condition-case err
(evil-narrow-to-line
(evil-forward-char count t noerror))
(error
;; Restore the previous command (this one never happend).
;; Actually, this preserves the current column if the
;; previous command was `evil-next-line' or
;; `evil-previous-line'.
(setq this-command last-command)
(signal (car err) (cdr err))))))
(t
(evil-motion-loop (nil (or count 1))
(forward-char)
;; don't put the cursor on a newline
(when (and evil-move-cursor-back
(not evil-move-beyond-eol)
(not (evil-visual-state-p))
(not (evil-operator-state-p))
(eolp) (not (eobp)) (not (bolp)))
(forward-char))))))
而左箭頭和h鍵,則是調用的evil-backward-char函數:
(evil-define-motion evil-backward-char (count &optional crosslines noerror)
:type exclusive
(interactive "<c>" (list evil-cross-lines
(evil-kbd-macro-suppress-motion-error)))
(cond
(noerror
(condition-case nil
(evil-backward-char count crosslines nil)
(error nil)))
((not crosslines)
;; restrict movement to the current line
(evil-with-restriction
(save-excursion
(evil-backward-char (1+ (or count 1)) t t)
(point))
(1+ (point))
(condition-case err
(evil-narrow-to-line
(evil-backward-char count t noerror))
(error
;; Restore the previous command (this one never happened).
;; Actually, this preserves the current column if the
;; previous command was `evil-next-line' or
;; `evil-previous-line'.
(setq this-command last-command)
(signal (car err) (cdr err))))))
(t
(evil-motion-loop (nil (or count 1))
(backward-char)
;; don't put the cursor on a newline
(unless (or (evil-visual-state-p) (evil-operator-state-p))
(evil-adjust-cursor))))))
上下移動
就是行間移動,標準emacs的方式是:
- 向下一行:C-n (next-line)
- 向上一行:C-p (previous-line) 這兩種方式在spacemacs中,在編輯模式下仍然可以使用。但是正常模式下已經被綁定到其他函數上了,因為有更方便的j和k可以用。
(defun next-line (&optional arg try-vscroll)
(declare (interactive-only forward-line))
(interactive "^p\np")
(or arg (setq arg 1))
(if (and next-line-add-newlines (= arg 1))
(if (save-excursion (end-of-line) (eobp))
;; When adding a newline, don't expand an abbrev.
(let ((abbrev-mode nil))
(end-of-line)
(insert (if use-hard-newlines hard-newline "\n")))
(line-move arg nil nil try-vscroll))
(if (called-interactively-p 'interactive)
(condition-case err
(line-move arg nil nil try-vscroll)
((beginning-of-buffer end-of-buffer)
(signal (car err) (cdr err))))
(line-move arg nil nil try-vscroll)))
nil)
spacemacs支持在普通模式下使用j來移動到下一行,k來移動到上一行。j綁定的是evil-next-line函數,k綁定的是evil-previous-line函數。
(evil-define-motion evil-next-line (count)
:type line
(let (line-move-visual)
(evil-line-move (or count 1))))
(evil-define-motion evil-previous-line (count)
:type line
(let (line-move-visual)
(evil-line-move (- (or count 1)))))
上麵兩個函數都是對evil-line-move的封裝,evil-next-line的參數是正的,evil-previous-line是負的。
(defun evil-line-move (count &optional noerror)
(cond
(noerror
(condition-case nil
(evil-line-move count)
(error nil)))
(t
(evil-signal-without-movement
(setq this-command (if (>= count 0)
#'next-line
#'previous-line))
(let ((opoint (point)))
(condition-case err
(with-no-warnings
(funcall this-command (abs count)))
((beginning-of-buffer end-of-buffer)
(let ((col (or goal-column
(if (consp temporary-goal-column)
(car temporary-goal-column)
temporary-goal-column))))
(if line-move-visual
(vertical-motion (cons col 0))
(line-move-finish col opoint (< count 0)))
;; Maybe we should just `ding'?
(signal (car err) (cdr err))))))))))
移動到行首或行尾
很多時候,我們需要移動到行首或行尾,而不是向左或向右一點一點移動。
我們還是先看emacs的標準實現方式:
- 到行首:C-a (move-beginning-of-line) spacemacs支持
- 到行尾:C-e (move-end-of-line) spacemacs不支持
move-beginning-of-line的實現如下:
(defun move-beginning-of-line (arg)
(interactive "^p")
(or arg (setq arg 1))
(let ((orig (point))
first-vis first-vis-field-value)
;; Move by lines, if ARG is not 1 (the default).
(if (/= arg 1)
(let ((line-move-visual nil))
(line-move (1- arg) t)))
;; Move to beginning-of-line, ignoring fields and invisible text.
(skip-chars-backward "^\n")
(while (and (not (bobp)) (invisible-p (1- (point))))
(goto-char (previous-char-property-change (point)))
(skip-chars-backward "^\n"))
;; Now find first visible char in the line.
(while (and (< (point) orig) (invisible-p (point)))
(goto-char (next-char-property-change (point) orig)))
(setq first-vis (point))
;; See if fields would stop us from reaching FIRST-VIS.
(setq first-vis-field-value
(constrain-to-field first-vis orig (/= arg 1) t nil))
(goto-char (if (/= first-vis-field-value first-vis)
;; If yes, obey them.
first-vis-field-value
;; Otherwise, move to START with attention to fields.
;; (It is possible that fields never matter in this case.)
(constrain-to-field (point) orig
(/= arg 1) t nil)))))
最終會調用到我們後麵要學的goto-char函數,通過goto-char跳到真正的位置上。
spacemacs支持vi的方式,在普通模式下,0移動到行首,$移動到行尾
- 0 (evil-digit-argument-or-evil-beginning-of-line)
- $ (evil-end-of-line)
evil-end-of-line其實還是要調用move-end-of-line函數來實現功能的。
(evil-define-motion evil-end-of-line (count)
:type inclusive
(move-end-of-line count)
(when evil-track-eol
(setq temporary-goal-column most-positive-fixnum
this-command 'next-line))
(unless (evil-visual-state-p)
(evil-adjust-cursor)
(when (eolp)
;; prevent "c$" and "d$" from deleting blank lines
(setq evil-this-type 'exclusive))))
移動到緩衝區的頭或尾
emacs的標準方式:
- 到緩衝區頭 A-< (beginning-of-buffer)
- 到緩衝區尾 A-> (end-of-buffer)
spacemacs支持這兩種方式,在正常模式下,還支持"<"鍵綁定beginning-of-buffer,">"綁定end-of-buffer的方式。
我們先看下beginning-of-buffer,雖然也是goto-char的封裝,但是確實不隻是(goto-char 0)這麼簡單:
(defun beginning-of-buffer (&optional arg)
(declare (interactive-only "use `(goto-char (point-min))' instead."))
(interactive "^P")
(or (consp arg)
(region-active-p)
(push-mark))
(let ((size (- (point-max) (point-min))))
(goto-char (if (and arg (not (consp arg)))
(+ (point-min)
(if (> size 10000)
;; Avoid overflow for large buffer sizes!
(* (prefix-numeric-value arg)
(/ size 10))
(/ (+ 10 (* size (prefix-numeric-value arg))) 10)))
(point-min))))
(if (and arg (not (consp arg))) (forward-line 1)))
end-of-buffer的話,除了goto-char之外,還得考慮recenter的問題
(defun end-of-buffer (&optional arg)
(declare (interactive-only "use `(goto-char (point-max))' instead."))
(interactive "^P")
(or (consp arg) (region-active-p) (push-mark))
(let ((size (- (point-max) (point-min))))
(goto-char (if (and arg (not (consp arg)))
(- (point-max)
(if (> size 10000)
;; Avoid overflow for large buffer sizes!
(* (prefix-numeric-value arg)
(/ size 10))
(/ (* size (prefix-numeric-value arg)) 10)))
(point-max))))
;; If we went to a place in the middle of the buffer,
;; adjust it to the beginning of a line.
(cond ((and arg (not (consp arg))) (forward-line 1))
((and (eq (current-buffer) (window-buffer))
(> (point) (window-end nil t)))
;; If the end of the buffer is not already on the screen,
;; then scroll specially to put it near, but not at, the bottom.
(overlay-recenter (point))
(recenter -3))))
移動到任意位置
emacs提供了兩個函數,可以跳到任意一行,或者是任意一個字符。
- A-g g 或 A-g A-g (goto-line n) :跳轉到第n行
- A-g c (goto-char n): 跳轉到第n個字符
spacemacs還支持vi的方式來跳轉行
- 行號 G (evil-goto-line),如果沒有行號,則跳到緩衝區末尾
goto-char不出意料的,是用C實現的。
我們先來看看goto-line:
(defun goto-line (line &optional buffer)
(declare (interactive-only forward-line))
(interactive
(if (and current-prefix-arg (not (consp current-prefix-arg)))
(list (prefix-numeric-value current-prefix-arg))
;; Look for a default, a number in the buffer at point.
(let* ((default
(save-excursion
(skip-chars-backward "0-9")
(if (looking-at "[0-9]")
(string-to-number
(buffer-substring-no-properties
(point)
(progn (skip-chars-forward "0-9")
(point)))))))
;; Decide if we're switching buffers.
(buffer
(if (consp current-prefix-arg)
(other-buffer (current-buffer) t)))
(buffer-prompt
(if buffer
(concat " in " (buffer-name buffer))
"")))
;; Read the argument, offering that number (if any) as default.
(list (read-number (format "Goto line%s: " buffer-prompt)
(list default (line-number-at-pos)))
buffer))))
;; Switch to the desired buffer, one way or another.
(if buffer
(let ((window (get-buffer-window buffer)))
(if window (select-window window)
(switch-to-buffer-other-window buffer))))
;; Leave mark at previous position
(or (region-active-p) (push-mark))
;; Move to the specified line number in that buffer.
(save-restriction
(widen)
(goto-char (point-min))
(if (eq selective-display t)
(re-search-forward "[\n\C-m]" nil 'end (1- line))
(forward-line (1- line)))))
evil-goto-line寫得簡短一些:
(evil-define-motion evil-goto-line (count)
:jump t
:type line
(if (null count)
(with-no-warnings (end-of-buffer))
(goto-char (point-min))
(forward-line (1- count)))
(evil-first-non-blank))
高效移動
重複執行命令
如果一行一行的移動,實在是太慢了,我們可以使用重複命令,給函數傳遞一個參數。
標準emacs的做法是Esc + 數字和C-u加數字兩種方式:
- Esc n + 命令:執行n次命令。如果無法執行完n次,就盡最大的努力。比如向下移動n行,到是沒到n行就到文件末尾了。那麼就停在文件末尾。 例: Esc 10 C-n,向下移動10行
- (universal-argument)函數,它綁定在C-u鍵上。 universal-argument如果不指定參數的話,默認執行4次。
但是在spacemacs上,universal-argument函數綁定在"空格 u"和"Alt-m u"兩個鍵上。
C-u在spacemacs中被移做綁定到evil-scroll-up上,用於翻屏。
居中重繪屏幕
有的時候,需要重新繪製一下屏幕,讓我們移動到的那行變為中心:
C-l (recenter-top-bottom)
(defun recenter-top-bottom (&optional arg)
"Move current buffer line to the specified window line.
With no prefix argument, successive calls place point according
to the cycling order defined by `recenter-positions'.
A prefix argument is handled like `recenter':
With numeric prefix ARG, move current line to window-line ARG.
With plain `C-u', move current line to window center."
(interactive "P")
(cond
(arg (recenter arg)) ; Always respect ARG.
(t
(setq recenter-last-op
(if (eq this-command last-command)
(car (or (cdr (member recenter-last-op recenter-positions))
recenter-positions))
(car recenter-positions)))
(let ((this-scroll-margin
(min (max 0 scroll-margin)
(truncate (/ (window-body-height) 4.0)))))
(cond ((eq recenter-last-op 'middle)
(recenter))
((eq recenter-last-op 'top)
(recenter this-scroll-margin))
((eq recenter-last-op 'bottom)
(recenter (- -1 this-scroll-margin)))
((integerp recenter-last-op)
(recenter recenter-last-op))
((floatp recenter-last-op)
(recenter (round (* recenter-last-op (window-height))))))))))
undo
做錯了,撤銷是很關鍵的操作。
在標準emacs中,使用undo函數來進行這個操作。它綁定到C-_或C-/或C-x u三個鍵上。
在spacemacs中,C-x u被綁定到undo-tree-visualize函數上。 還可以用"空格 a u"來訪問它。
(defun undo-tree-visualize ()
"Visualize the current buffer's undo tree."
(interactive "*")
(deactivate-mark)
;; throw error if undo is disabled in buffer
(when (eq buffer-undo-list t)
(user-error "No undo information in this buffer"))
;; transfer entries accumulated in `buffer-undo-list' to `buffer-undo-tree'
(undo-list-transfer-to-tree)
;; add hook to kill visualizer buffer if original buffer is changed
(add-hook 'before-change-functions 'undo-tree-kill-visualizer nil t)
;; prepare *undo-tree* buffer, then draw tree in it
(let ((undo-tree buffer-undo-tree)
(buff (current-buffer))
(display-buffer-mark-dedicated 'soft))
(switch-to-buffer-other-window
(get-buffer-create undo-tree-visualizer-buffer-name))
(setq undo-tree-visualizer-parent-buffer buff)
(setq undo-tree-visualizer-parent-mtime
(and (buffer-file-name buff)
(nth 5 (file-attributes (buffer-file-name buff)))))
(setq undo-tree-visualizer-initial-node (undo-tree-current undo-tree))
(setq undo-tree-visualizer-spacing
(undo-tree-visualizer-calculate-spacing))
(make-local-variable 'undo-tree-visualizer-timestamps)
(make-local-variable 'undo-tree-visualizer-diff)
(setq buffer-undo-tree undo-tree)
(undo-tree-visualizer-mode)
;; FIXME; don't know why `undo-tree-visualizer-mode' clears this
(setq buffer-undo-tree undo-tree)
(set (make-local-variable 'undo-tree-visualizer-lazy-drawing)
(or (eq undo-tree-visualizer-lazy-drawing t)
(and (numberp undo-tree-visualizer-lazy-drawing)
(>= (undo-tree-count undo-tree)
undo-tree-visualizer-lazy-drawing))))
(when undo-tree-visualizer-diff (undo-tree-visualizer-show-diff))
(let ((inhibit-read-only t)) (undo-tree-draw-tree undo-tree))))
而C-_,C-/,在spacemacs中,被綁定在undo-tree-undo上。
小結
功能 | 函數名 | 快捷鍵 | leader鍵 |
---|---|---|---|
光標右移 | forward-char | 無 | 無 |
evil-forward-char | l | 無 | |
光標左移 | backward-char | 無 | 無 |
evil-backward-char | h | 無 | |
下移一行 | next-line | 正常模式C-n無效 | 無 |
evil-next-line | j | 無 | |
上移一行 | previous-line | 正常模式C-p無效 | 無 |
evil-previous-line | k | 無 | |
光標移至行首 | move-beginning-of-line | C-a | 無 |
evil-digit-argument-or-evil-beginning-of-line | 0 | 無 | |
光標移至行尾 | move-end-of-line | 無 | 無 |
evil-end-of-line | $ | 無 | |
跳轉到某一行 | goto-line | A-g g或A-g A-g | 無 |
evil-goto-line | G | 無 | |
跳到某一字符 | goto-char | A-g c | 無 |
跳到緩衝區頭 | beginning-of-buffer | A-<或> | 無 |
跳到緩衝區尾 | end-of-buffer | A->或> | 無 |
重複執行 | universal-argument | A-m u | 空格 u |
居中重繪屏幕 | recenter-top-bottom | C-l | 無 |
撤銷上一次的操作 | undo | 無 | 無 |
undo-tree-visualize | C-x u | 無 | |
undo-tree-undo | C-_或C-/ | 無 |
最後更新:2017-06-02 19:36:02