我也说说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-/
时间: 2024-11-01 14:29:26

我也说说Emacs吧(4) - 光标的移动的相关文章

Emacs之魂(二):一分钟学会人界用法

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 上文提到了编辑器之战, 据江湖传说,Emacs被称为"神的编辑器", Emacs有着无与伦比的可扩展性和可定制性,简直变成了一个"

Emacs之魂(四):标识符,符号和变量

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 1. 符号 上文我们提到了Emacs Lisp是一种Lisp-2, 即同一个符号(symbol)在不同的上下文中,可以分别表示两种不同的值(value): 变量

emac-Emacs 自动补全 auto-complete yasnippet 光标空白处不显示

问题描述 Emacs 自动补全 auto-complete yasnippet 光标空白处不显示 我在ubuntu中配置了emacs 的自动补全,现在碰到一个问题,在出现自动补全的时候,光标在有字符的地方会闪烁,在没有字符或者空白处无法看到光标,请问怎么让光标都在空白处也显示 下面的是我自动补全的配置 ;; yasnippet (add-to-list 'load-path "~/.emacs.d/yasnippet-0.6.1c") (require 'yasnippet);; no

Emacs常用命令汇总

注意:以下命令中标注的按键,大写的C代表Control,在键盘上通常是Ctrl键,而M代表Meta,在键盘上通常是Alt键,S则代表Shift,在键盘上通常是Shift键,也就是 C Control M Alt S Shift 这三个键在Emacs里通常作为组合键的前导按键使用,也就是说,执行一条命令前可能需要按住这个键不放,比如搜索命令是C-s,要执行这个命令首 先要按住Ctrl键不放,再按下字母s键:而打开文件命令是C-x C-f,要打开文件就必须按下Ctrl键不放,依次按下x和f(当然也可

我也说说Emacs吧(1) - Emacs和Vi我们都学

好友幻神的<Emacs之魂>正在火热连载中,群里人起哄要给他捧捧场. 作为一个学习Emacs屡败屡战的用户,这个场还是值得捧一下的.至少我是买了HHKB键盘的... 从我的键盘说起 - 有模式和无模式 下面是我的HHKB键盘的局部图: 与其他常规的键盘不同,我的键盘的Control键的位置,是常规布局的大小写锁定键的位置.为什么这么布局呢? 我们看看幻神在emacs人界用法中所介绍的emacs最常的快捷键吧: C-f 后一个字符 C-b 前一个字符 C-p 上一行 C-n 下一行 M-f 后一

Ubuntu 安装 Emacs

emacs 目前正式发布的最新版本是 21.4,这个版本在 Ubuntu 下对中文以及中文输入法的支持多少有点问题,所以我们可以考虑从 emacs cvs 仓库中获取最新的 23.x (emacs-unicode-2) 版本,此版本很好的解决了中文显示以及 Gnome 下中文输入法的问题.以下操作基于 Ubuntu 6.10 环境: 1.从 cvs 仓库取出最新源代码: Ubuntop:~$ set CVS_RSH="ssh" ## 如果你使用的是 Bash,使用 export CVS

emacs+ensime+sbt打造spark源码阅读环境

概述 Scala越来越流行, Spark也愈来愈红火, 对spark的代码进行走读也成了一个很普遍的行为.不巧的是,当前java社区中很流行的ide如eclipse,netbeans对scala的支持都不算太好.在这种情况下不得不想到编辑器之神emacs,利用emacs+ensime来打造scala编程环境. 本文讲述的步骤全部是在arch linux上,其它发行版的linux视具体情况变通. 安装scala pacman -S scala 安装sbt pacman -S sbt 安装ensim

Emacs 中给文本加引号的插件

前几天 @刘鑫-MarchLiu 在微博上发布了一个给给文本加引号的插件:http://weibo.com/1729408273/eDcC8e8w6aD.不过用起来有点小问题: 两头都只能插入一个字符,因此不能用于添加 XML 标签: 光标控制上有个 bug,每次执行后光标会往左移动一个字符. 我自己刚刚也实现了一下,不过我的实现灵活性比较差(前后的符号必须由用户手工输入,不能以参数形式传递): (defun wrap-thing (thing) "Wrap the thing at point

我也说说Emacs吧(6) - Lisp速成

前面我们学习了基本操作,也走马观花地看了不少emacs lisp的代码.这一章我们做一个lisp的速成讲座. Lisp的含义是表处理语言.它的代码组成结构都是用括号组成的表来表示的.Lisp中的功能,要么是以函数形式求值,要么本身就是一些特殊表. 比如在Lisp语言中,判断分支的if不是语句,也不是函数,而是一种特殊的表.定义函数的方式,也是用一种叫做defun的特殊表. Lisp基本函数速成 首先我们搭建一下环境,随便建一个.el为扩展名的文件.然后,我们写一个helloworld的代码吧: