移动命令使用频率非常之高,只要方法多一个小小的弯路,对使用体验影响都很大。
克服移动上的难度,离掌握Emacs就不远了。
在不安装其它包的情况下,Emacs就可以:
- 以行为单位移动: C-n/C-p
- 以段落为单位移动:C-Down/C-Up
但能快速移动到某一行,这种方式太低效了,就算是使用C-r/C-s搜索字符,也非常的麻烦。
本文以上下移动命令为例,利用人的本能条件反射,实现高效的移动方式。
基本思想
- 利用本能反应,即目标在哪就往哪里移动
- 对不精确的操作再修正
具体步骤是:
- 目标在光标上方,就向上移动,反之向下
- 移动的幅度为当前位置到边界的一半
- 再重复1,2步骤,直到满意为止
整个过程利用二分法定位。
动图演示,当前光标在文件底部,目标位置是NOTE那行注释:
avy之类的第三方包有类似的功能,但思想完全不一样。
它些包基本是为每行生成一些标记,再通过输入标记从而定位到对应的位置。
这些方法你需要观察标记,再输入标记。但读取标记是要经过大脑思考的,并不是人的本能反应,所以使用起来并不流畅。
类比于开车转弯:
当想向右拐弯的时候,向右打方向盘是不用思考的,只是打多打少的问题。如果车辆打过了,再向左打以校正,重复这个过程直到车辆方向正确。
这个过程是非常自然的,人脑不是擅长这种精密操作,有一个八九不离十再修正就可以了。
具体实现
Margin处的颜色与字符是用来预测下两步操作的,选择JK是因为这个思想我先在Vim上实现,JK是Vim的移动方式。
Margin上的标识比Vim中的Sign方便,不像Vim最多两个字符,而且在Bash上也可以正常渲染。
关键代码并不多,大部分是细节优化,比如到达边界自动翻页、跳转到上次位置、跳过折叠的内容等。
(global-set-key (kbd (concat custom-user-prefix-key "k")) (lambda () (interactive) (custom/bisect-jump-2 t)))
(global-set-key (kbd (concat custom-user-prefix-key "j")) 'custom/bisect-jump-2)
(defun custom/bisect-jump-2 ( &optional is-up )
"Bisect jump."
(interactive)
(setq old-hl-line-flag nil)
(if (boundp 'hl-line-mode)
(setq old-hl-line-flag hl-line-mode)
)
(create-all-bisect-jump-marks)
(set-window-margins nil 2)
(setq hit-radus 0)
(setq current-position (count-screen-lines (window-start) (point)))
(setq top-line 1)
(setq bottom-line (- (count-screen-lines (window-start) (window-end)) 1))
(setq done nil)
(setq old-position (point))
(setq reach-end nil)
(setq move-down nil)
(setq move-up nil)
(setq is-quick nil)
(while (not done)
(setq old-position (point))
(if reach-end
(progn
(setq top-line 1)
(setq bottom-line (- (count-screen-lines (window-start) (window-end)) 1))
(setq reach-end nil)))
;; (if (<= (1+ (* 2 hit-radus)) (- bottom-line top-line))
(if (and (> bottom-line top-line) (not move-down) (not move-up))
(if is-up
(progn
(setq bottom-line (- (- current-position hit-radus) 1) )
(if is-quick
(setq top-line (/ (+ top-line bottom-line) 2))))
(progn
(setq top-line (+ current-position hit-radus 1))
(if is-quick
(setq bottom-line (/ (+ top-line bottom-line) 2))))))
(if (>= top-line bottom-line)
(progn
(if is-up
(setq bottom-line top-line)
(setq top-line bottom-line))
(setq reach-end t)))
(setq flags (generate-flags (1+ (- bottom-line top-line)) (+ 1 (* 2 hit-radus))))
(if (and (not move-down) (not move-up))
(setq current-position (/ (+ top-line bottom-line) 2))
(progn
(if move-down
(setq current-position (1+ current-position))
(setq current-position (1- current-position))
)
)
)
(goto-char (window-start))
(vertical-motion (- current-position 1))
;; (global-hl-line-highlight)
(hl-line-mode)
(clear-all-bisect-jump-marks)
;; render the position
(setq i 0)
(while (< i (length flags))
(setq item (nth i flags))
(setq line (+ i top-line))
(setq position (custom/pos-at-screen-line-col line 0))
(if (or (= 0 (nth 0 item)) (> 2 (length item)))
(mark-in-margin i position (nth 0 item) 0)
(progn
(setq first-element (nth 0 item))
(setq second-element (nth 1 item))
(mark-in-margin i position first-element second-element)
)
)
(setq i (1+ i)))
(if (and (= old-position (point)) reach-end)
(progn
(setq top-line 1)
(setq bottom-line (- (count-screen-lines (window-start) (window-end)) 1))
(if is-up
(recenter (- bottom-line 2) t)
(recenter 1 t)
)
(setq current-position (/ (+ top-line bottom-line) 2))
(setq top-line current-position)
(setq bottom-line current-position)
)
)
(if reach-end
(message "Reach the finish line, there's no room to jump yet!")
(message "Bisect jump. Type j/k or M-j/k to jump up or down, ' to jump back and 'q' to quit."))
(setq move-down nil)
(setq move-up nil)
(setq is-quick nil)
(setq key 0)
;; https://stackoverflow.com/questions/884498/how-do-i-intercept-ctrl-g-in-emacs
(let ((inhibit-quit t))
(unless (with-local-quit
(setq key (read-key "read-key"))
;; read-event is affected by message statement, quite weird.
;; (setq key (read-event "todo"))
t)
(progn
(setq key ?q)
(message "C-g hit")
(setq quit-flag nil)
)))
(cond
( (equal key ?j)
(setq is-up nil))
( (equal key ?k)
(setq is-up t))
( (equal key ?n)
(setq is-quick t)
(setq is-up nil))
( (equal key ?p)
(setq is-quick t)
(setq is-up t))
( (equal key ?q)
(setq done t))
( (or (equal key ?') (equal key ?\;) )
(goto-char old-position)
(setq done t))
(t
(setq done t)
;; apply the current key press
(setq unread-command-events
(append (listify-key-sequence (vector key))
unread-command-events))
)
)
(if done
(progn
(clear-all-bisect-jump-marks)
(message "Quit bisect-jump.")
))
)
(unless old-hl-line-flag
(hl-line-mode -1)
)
)
其中generate-flags是用于计算预测位置的:
(defun generate-flags (num width)
(let
((flags '())
(flags-1 '())
(pos1 0)
(pos2 0)
(i 0))
(if (<= num width)
(setq flags (make-list num '(0)))
(progn
(setq pos1 (/ (- num width) 2))
(setq pos2 (+ pos1 width))
; first part [0, pos1)
(setq flags (make-list pos1 '(1)))
(if (> pos1 width)
(progn
(setq flags-1 (generate-flags pos1 width))
(setq i 0)
(while (< i pos1)
(setf (elt flags i) (append (nth i flags) (nth i flags-1)) )
(setq i (1+ i))
)
)
)
; middle part [pos1, pos2)
(setq flags (append flags (make-list (- pos2 pos1) '(0))))
; last part [pos2, num)
(setq flags (append flags
(make-list (- num pos2) '(2))
))
(if (> (- num pos2) width)
(progn
(setq flags-1 (generate-flags (- num pos2) width))
(setq i 0)
(while (< i (- num pos2))
(setf (elt flags (+ pos2 i)) (append (nth (+ pos2 i) flags) (nth i flags-1)) )
(setq i (1+ i))
)
)
)
)
)
flags
)
)
至此,你不需要安装额外的包就解决了光标移动的效率问题。