做异端中的异端 -- Emacs裸奔之路5: 条件反射式移动

移动命令使用频率非常之高,只要方法多一个小小的弯路,对使用体验影响都很大。

克服移动上的难度,离掌握Emacs就不远了。

在不安装其它包的情况下,Emacs就可以:

  • 以行为单位移动: C-n/C-p
  • 以段落为单位移动:C-Down/C-Up

但能快速移动到某一行,这种方式太低效了,就算是使用C-r/C-s搜索字符,也非常的麻烦。

本文以上下移动命令为例,利用人的本能条件反射,实现高效的移动方式。

基本思想

  • 利用本能反应,即目标在哪就往哪里移动
  • 对不精确的操作再修正

具体步骤是:

  1. 目标在光标上方,就向上移动,反之向下
  2. 移动的幅度为当前位置到边界的一半
  3. 再重复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
    )
)

至此,你不需要安装额外的包就解决了光标移动的效率问题。

相关推荐
潇-xiao43 分钟前
vim的相关命令 + 三种模式(10)
linux·编辑器·vim
程序猿小D1 小时前
第28节 Node.js 文件系统
服务器·前端·javascript·vscode·node.js·编辑器·vim
kooboo china.4 小时前
什么是JSON ?从核心语法到编辑器
javascript·编辑器·json
waterHBO14 小时前
Cursor 编辑器, 使用技巧,简单记录一下
windows·编辑器
程序猿小D17 小时前
第24节 Node.js 连接 MongoDB
数据库·mongodb·npm·node.js·编辑器·vim·express
FL162386312919 小时前
VScode打开后一直显示正在重新激活终端 问题的解决方法
ide·vscode·编辑器
程序猿小D21 小时前
第26节 Node.js 事件
服务器·前端·javascript·node.js·编辑器·ecmascript·vim
朝阳391 天前
Electron-vite【实战】MD 编辑器 -- 编辑区(含工具条、自定义右键快捷菜单、快捷键编辑、拖拽打开文件等)
javascript·electron·编辑器
朝阳391 天前
Electron-vite【实战】MD 编辑器 -- 大纲区(含自动生成大纲,大纲缩进,折叠大纲,滚动同步高亮大纲,点击大纲滚动等)
javascript·electron·编辑器
晨曦backend2 天前
Vim 匹配跳转与搜索命令完整学习笔记
linux·编辑器·vim