auto-modal 模态自动切换

大家新年好!最近撸了一个package,欢迎大家试用,提出你的宝贵建议!

在 emacs 中,对于需要频繁使用的命令,我们倾向于将其绑定到快捷键,即按下几个组合按键便完成了一个复杂功能的调用。可以将任意的命令绑定到快捷键是 emacs 被认为灵活、高效的重要原因之一。emacs 中的快捷键的特点是需要使用前缀键,即 C-cC-x 等,但这比起 vi 这种模态编辑中使用单字母按键,又显的不那么的“快捷”了。习惯了 vi 的模态切换方式的用户可能会在 emacs 中使用诸如 evil-modemeow 等方案来延续这种使用习惯。我本人不用“模态编辑”,但又觉得单字母按键确实高效,于是便思考一种即能够使用上单字母快捷键,但又无需主动切换模态的方案,于是有了 auto-modal 这个 package。

Auto-modal 做了什么

Auto-modal 顾名思义被称为“自动模态切换”。当光标所在位置满足一个指定的断言函数时,自动切换到“命令模式”(这里所说的命令模式指的是可以通过预先定义的多个单字母按键触发命名的执行);当光标所在位置不满足指定的断言函数时,自动切换到普通模式(这里说的普通模式就是正常使用 emacs 的状态)。在这两种切换的过程中,任何其他的按键绑定都不会受到影响。

在 emacs 的正常编辑中,字母(数字,标点)按键默认被绑定到 self-insert-command,即在光标位置插入键入的字符。显然,在这种编辑情况下,切换到命令模态是不合适的。于是我们需要寻找一些在编辑中的 rare cases,在这些情况中,我们鲜少输入字符,并将其作为自动切换模态的触发情况。比如,在已有文本的一行的开头 或 lisp系语言左括号的左边 等位置。当我们需要触发字母按键的命令时,只需以日常移动光标的代价来达到这种状态,执行完命令,然后再移动光标,继续编辑… 整个过程,我们无需关心当前处于哪种模态,因为可以定义这两种不同状态下显示不同的光标样式,在模态自动切换时,光标样式也会自动切换。

更精细化的自动切换

如果对于所有 buffer 我们只能定义一种或几种自动触发模态切换的条件,那么 auto-modal 的功能将会变得很局限。因为不同的文本结构,同一种触发条件并不适合所有情况;而如果绑定所有可能的触发位置,对于无需切换的情况,又会变得冗余。Auto-modal 支持将同一个按键绑定到不同的 major mode 下的不同的触发条件中。

举个例子:

(auto-modal-bind-key "j" 'org-mode 'auto-modal-bolp 'dired-jump)
(auto-modal-bind-key "j" 'emacs-lisp-mode 'auto-modal-before-parensp 'auto-modal-next-parens)
(auto-modal-bind-key "j" 'emacs-lisp-mode 'auto-modal-bolp 'auto-modal-next-function)
(auto-modal-bind-key "j" 'fundamental-mode 'auto-modal-bolp 'auto-modal-next-line)
  • 第一个绑定:在 org-mode 中,当满足 auto-modal-bolp 断言时,按 “j” 跳转到当前的 dired 目录
  • 第二个绑定:在 emacs-lisp-mode 中,当满足 auto-modal-before-parensp 断言时,按 “j” 跳转到下一个括号的开头
  • 第三个绑定:在 emacs-lisp-mode 中,当满足 auto-modal-bolp 断言时,按 “j” 跳转到下一个函数
  • 第四个绑定:在所有的 major mode 中,当满足 auto-modal-bolp 断言时,按 “j” 跳转到下一行

(以上示例中的函数只作为理解例子使用,未提供实际函数)

值得注意的是,按键绑定可以随着 major mode 的继承而继承,而子 major mode 指定了同一按键、同一触发条件的函数时,会覆盖父 major mode 的绑定。上面的第三个绑定就覆盖了第四个的行为,如果没有其他的绑定,在所有非 emacs-lisp-mode 中,满足 auto-modal-bolp 断言的所有的按键 “j”,都会触发跳转到下一行。

命令模式下需要输入字符时如何处理?

虽然在触发位置输入字符被认为是 rare case,但也会存在需要输入的场景,此时就需要主动切换了插入模态了。使用内置命令 auto-modal-enable-insert 主动切换到插入模式,你可以将它绑定到一个字母按键。

我的配置

auto-modal 是一个高度可定制化的模态自动切换系统,用户可能根据自己的需求,进行个性化的配置或发现更多有趣的用法。如果你还不清楚该如何使用,下面是目前我个人的配置,供大家参考。

use-region-p

当选中 region 时,设置一些字母按键操作选中的文本或执行其他命令

(defun auto-modal-set-cursor-when-idle ()
  "Set cursor type correctly in current buffer
after idle time. It's useful when `use-region-p'
is the predicate function."
  (interactive)
  (when (use-region-p)
    (run-with-idle-timer 0.1 nil 'auto-modal-set-cursor)))

;; delay update cursor-type when use-region-p
(add-hook 'post-command-hook 'auto-modal-set-cursor-when-idle)

(auto-modal-bind-key "u" 'global 'use-region-p 'upcase-dwim)
(auto-modal-bind-key "d" 'global 'use-region-p 'downcase-dwim)
(auto-modal-bind-key "c" 'global 'use-region-p 'kill-ring-save)
;; ......

bolp

我喜欢将光标位于一行开头(排除空字符行的开头)这个位置作为触发模态自动切换的触发条件,除了它是输入字符的 rase case,移动光标到开头也是日常编辑中非常频繁的操作。

(defun auto-modal-bolp ()
  (and (bolp) (not (looking-at "^$"))))

(defun auto-modal-next-line ()
  (interactive)
  (forward-line 1)
  (goto-char (line-beginning-position))
  (while (and (not (= (point) (point-max)))
              (looking-at "^$"))
    (auto-modal-next-line)))

(defun auto-modal-previous-line ()
  (interactive)
  (forward-line -1)
  (goto-char (line-beginning-position))
  (while (and (not (= (point) (point-max)))
              (looking-at "^$"))
    (auto-modal-previous-line)))

(auto-modal-bind-key "l" 'global 'auto-modal-bolp 'avy-goto-line)
(auto-modal-bind-key "c" 'global 'auto-modal-bolp 'avy-goto-char-timer)
(auto-modal-bind-key "j" 'global 'auto-modal-bolp 'auto-modal-next-line)
(auto-modal-bind-key "o" 'global 'auto-modal-bolp 'other-window 1)
(auto-modal-bind-key "k" 'global 'auto-modal-bolp 'auto-modal-previous-line)
(auto-modal-bind-key "SPC" 'global 'auto-modal-bolp 'auto-modal-enable-insert)
(auto-modal-bind-key "f" 'global 'auto-modal-bolp 'counsel-find-file)
(auto-modal-bind-key "<" 'global 'auto-modal-bolp 'backward-page)
(auto-modal-bind-key ">" 'global 'auto-modal-bolp 'forward-page)

vi-mode

将触发条件设为一个始终返回 t 的函数,auto-modal 便退化为了 vi 的主动模态切换,下面是使用 auto-modal 配置的一个简易的 vi-mode 实现。

(defvar auto-modal-vi-keybinds
  '(("i" auto-modal-vi-insert-mode)
    ("j" next-line)
    ("k" previous-line)
    ("z" forward-line 2)
    ("h" backward-char)
    ("l" forward-char)
    ("w" forward-word)
    ("b" backward-word)))

(defvar auto-modal-vi-insert-flag nil
  "When `auto-modal-vi-insert-flag' is nil,
it's in vi normal mode. Otherwise, it's in
vi insert mode.")

(defun auto-modal-vi-pred () t)

(defun auto-modal-vi-normal-mode ()
  (setq auto-modal-vi-insert-flag nil)
  (dolist (keybind auto-modal-vi-keybinds)
    (apply 'auto-modal-bind-key
           (car keybind) 'global 'auto-modal-vi-pred (cdr keybind))))

(defun auto-modal-vi-insert-mode ()
  (setq auto-modal-vi-insert-flag t)
  (auto-modal-unbind-with-predicate 'auto-modal-vi-pred))

(defun auto-modal-vi-mode-toogle ()
  (interactive)
  (if auto-modal-vi-insert-flag
      (auto-modal-vi-normal-mode)
    (auto-modal-vi-insert-mode)))

(defvar auto-modal-vi-keymap
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "<escape>") 'auto-modal-vi-mode-toogle)
    map))

;;;###autoload
(define-minor-mode auto-modal-vi-mode
  "Auto-modal vi mode"
  :global t
  :keymap auto-modal-vi-keymap
  (unless auto-modal-mode (auto-modal-mode 1))
  (if auto-modal-vi-mode
      (auto-modal-vi-normal-mode)
    (auto-modal-vi-insert-mode)))

其他

还有一些其他的有用、有趣的用法,欢迎大家探索。比如将 lisp 系语言的左括号作为切换的触发点,便可以为S表达式的光标移动和表达式操作提供更灵活的操作方式…

3 个赞

有点类似 org 的 speed-command。跟模态编辑相比,感觉使用负担更大,要使用者记住这些 rare cases 。

2 个赞

不需要记住,光标样式会自动切换来提示。目前默认的配置是,命令模式是box,正常模式是 bar。我目前用起来还是比较爽的,没有什么负担,你可以尝试一下。

1 个赞

有光标提示要好些,如果能在 echo 区域简短提示下要用的快捷键就更好了 :smile:

你举的这个例子:

(auto-modal-bind-key "j" 'emacs-lisp-mode 'auto-modal-before-parensp 'auto-modal-next-parens)
(auto-modal-bind-key "j" 'emacs-lisp-mode 'auto-modal-bolp 'auto-modal-next-function)

如果我要正常输入 j 尼?

好主意,确实可以提示绑定了哪些按键,空了搞一下。

如果要输入字符,看这段:

哈哈,我之前也思考过能不能把 emacs 的 按键设计与 vim 的结合起来,但都没想到好的办法。之前用 evil 时都会在 insert-mode 下绑定常用的 emacs 按键,这样就不用经常切换模态,也能既输入又编辑。后面也尝试了 meow ,它的方案我感觉可能是最好的了。但后面都由于增加了额外的负担而放弃了。

我感觉这个问题可以再深入思考下🤔,想找一个负担小,又按键少,能同时结合 emacs 和 模态输入的按键设计。

1 个赞

更改光标的做法感觉不是很明显, 个人觉得可能在可以触发的时候 光标 更换颜色更容易识别?
还有另外的, 如果真的是用颜色做区分, 还希望将auto-modal 光标继承 emacs 默认的 cursor-type 而不是另定义一个 auto-modal 光标

当然, 这是都是个人愚见, 你有的你的考虑, 不要放心上

1 个赞

谢谢你的建议,我是没有想过通过光标颜色来区分编辑模式和命令模式,因为大多数的模态切换都是使用 bar(插入) 和 box(命令) 这两种不同的光标样式来区分,可能大家更习惯这种方式?

但你说的这种情况我觉得也是有必要支持的。考虑到不破坏用户原有的光标样式,我会新增两个选项来设置不同状态下光标的颜色,同时保留原本的光标样式的选项,用户可以设置为继承原有的样式。如何设置交给用户按照偏好自定义。

没看明白,有无 gif 可以简单看看是咋回事儿么

完善的差不多了再录个gif

更新:支持设置光标颜色

  • auto-modal-control-cursor-typeauto-modal-control-cursor-type 值为 'default 时,表示使用用户默认的光标样式,其余设置的值和 cursor-type 一样。

  • auto-modal-control-cursor-colorauto-modal-insert-cursor-color 用于设置光标的颜色,如何使用看变量的文档:

(setq auto-modal-control-cursor-type 'box
      auto-modal-insert-cursor-type 'default
      auto-modal-control-cursor-color 'warning
      auto-modal-insert-cursor-color nil

更新:支持在进入触发区域时,提示按键

默认关闭该选项,设置 (setq auto-modal-enable-keyhint t) 开启。由于频繁提示按键会很 annoying,只支持在刚进入触发区域时,提示一次。

效率好高。

确实会有点 annoying,我感觉设计成一个按键,然后弹出对应帮助可能会好些。比如 org 开启 speed-command,在每个 headline 的开头按 “?” 就会弹出按键的帮助信息。

1 个赞

我一直对你的 bujo-mode 很感兴趣,现在是已经开发完了吗?

开发中,我就是在开发 bujo-mode 的过程中才想到的 auto-modal.el,然后就偏离了主线哈哈哈。还有另外一个 package: bullet.el 在完善中,bujo-mode 将会由 auto-modal.elbullet.el 这两个通用 package 的功能配合实现 (或者将两者的代码整合到 bujo-mode 中,这样不用安装额外的package),但是前面两个通用的单独的 package 不仅限于 bujo-mode,所以先单独发布出来。

1 个赞

这个想法不错!

更新:在触发位置主动按键,提示当前绑定的所有命令

设置该按键的变量是 auto-modal-help-key,默认值是 ?

更新:配置了一组S表达式的快捷键