【求助】如何避免 Emacs 递归映射,即同时设置 `C-c d` 和 `C-c d d`?谢谢 🙏

(use-package osx-dictionary
  :bind
  ("C-c d d" . osx-dictionary-search-word-at-point)
  )

(use-package emacs
  :ensure nil
  :defer t
  :bind
  (
   ("C-c d" . duplicate-dwim)
  )
  )

以上设置会报错:

error: Key sequence C-c d d starts with non-prefix key C-c d

设置一个 ctl-c-d-map 是不是可以解决问题呢?求大佬指点迷津。感谢感谢 :pray:

C-c d 绑定一个命令, 这个命令做两件事情, 一个是等用户输入字符 (read-char), 同时加一个0.5s 的计时器。

如果计时器到了就执行 C-c d 的命令, 如果计时器还没有结束, 就执行 read-char 对应的命令。

3 个赞

不知道 Emacs 中有没有类似 Vim 的递归映射/非递归映射。

很久没用 Vim 了,也不记得自己设置过这样拧巴/奇葩的俩快捷键,不过感觉这样可以“复用”快捷键的一部分的思路有点搞头。

如果最后两个按键的时间间隔(interval)以及最后一个按键和快捷键生效前的时间间隔(timeout)都是可调的话,那就完全不需要单独定义一个 prefix-map 了。

哇,这个思路简洁优美!

如果 Emacs 有按键间隔和 timeout 的概念就好了。

run-with-timer

1 个赞

谢谢猫大 :pray:

:rofl: 我是狗熊啃棒子型的,我再学学怎么把这个功能写出来。

general可以很方便地绑 C-c d d & C-c d e,和你的想法略有区别。不过还是多按个键比等0.5秒更快更可靠吧?

以上描述有点问题,详见general-key-dispatch

大概可以做一个C-c d打头的hydra?

我后来又翻了不少帖子,查了一些资料,最后还是放弃了。

不是 timeout 大小的问题,也不是代码多难学多难写的问题,而是理念问题。

如果要实现“前缀”复用(随手起的名字),即让不同的 key sequence 具有相同的“前缀”,就需要把每个 sequence 的“前缀”部分变成一个特殊的 keymap/keyboard-macro。也就是要考虑所有按键序列和单个按键是不是都是特殊的 keymap/macro。

如果 Emacs 默认所有的按键序列中的每个按键都是一棵树上的分支,那么只需要一个 timeout 作为分支的末端标记点就够了,但现实情况是 Emacs 没有这么做。

如你所说,还不如利用目前可以写进 keymap 的键。

所以我今天也进行了一些快捷键的调整,摸索适合自己的键序列。

我一开始想的是如何避免 Emacs 递归映射,结果最后发现恰恰是因为 Emacs 缺少递归。

比如 ESC a 只绑定了 backward-sentence 而不是作为 esc-a-map 的 timeout, ESC e 只绑定了 fowward-sentence 而不是作为 esc-e-map 的 timeout,相当于这条路堵死了(或者说这里本来就是断头路)。

这应该是一个left recursion的问题。

不是程序员,看不太懂 :rofl:

做为可扩展性最强的编辑器,key sequence 里居然有断头路,这是最让我百思不得其解的。

主要是想不通 key sequence 如何判断自己已经结束了该去执行对应的 command,如果能够参与控制 key sequence 何时结束问题就简单了。猫大的思路可以解决一部分问题,最根本的问题是断头路修起来太费劲了。

timeout我试过,不太好用,我试的是“esc和其它键一起按就当作ctrl,0.5s内没有别的键被按下就发送esc”,试下来很不好用,0.5s还是0.2s这个thresh很难定,要么你等它,要么它等你,很难受。

但是我这个如果能成功还是很有用的,节约一个物理按键。键盘大小是有限的。

而你这个情况,意义不大,只节约出一个短sequence,能给C-c d单独一个按键,这个目的你不用timeout,直接在C-c a x这里多挤一个就能把C-c d省出来。

至于到底哪个该用更短的键,可以用keyfreq统计自己1周/1个月每个键都按了几次

1 个赞

timeout 是任何一个无法做到全键无冲的键盘或者映射软件都有的特性,而 emacs 的 key sequen 是一颗树,只需要让它无限分叉,最后一个按键是终点,比如 C-c M-k 其实是 C-c 之后单独按下 ESC 和 k 之后 Emacs 读取的最后结果,这三次按键中间的两个间隔可以是不固定(一直等待终点按键的出现)的。

我之前在不知道 doom 是把 短时间连按 jk 作为esc在的功能在 vterm 模式禁掉了,所以以为是在 vterm 实现不了。就自己在 vterm 里糊了一个类似的功能的函数。这样子做的好处就是按 j 的时间不会卡住,而是会立即输入字母 j,然后短时间内按 k 就会退出插入模式;然后把之前的 j 给删掉。我在 vim 里面甚至也是这么配置的;而并没有利用vim支持的原生的递归映射。因为 vim 的原生递归映射会导致你按j的那一瞬间会卡0.1秒,直到你在这个时间内没有按 k,才会输入 j 字符。

我的 lisp 水平很垃圾,就是单纯的纯粹纸糊的水平。能达成功能就完事。


(defun my-vterm-k-as-switch-to-normal-state ()
  "send backspace firstly (to delete char ?j previously inserted) and then switch to normal state."
  (interactive)
  (vterm-send-backspace)
  (evil-force-normal-state))

(defun my-vterm-j-set-k-as-switch-to-normal-state ()
  "After inserting ?j, map ?k to `my-vterm-k-as-switch-to-normal-state',
and after 0.1s to map ?k to its default: `vterm--self-insert'"
  (interactive)
  (vterm--self-insert)
  (map! :map vterm-mode-map :i "k" #'my-vterm-k-as-switch-to-normal-state)
  (run-with-idle-timer 0.1 nil
                       (lambda ()
                         (map! :map vterm-mode-map :i "k" #'vterm--self-insert))))

(after! vterm
  (map! :map vterm-mode-map
        :i "C-c <escape>" #'vterm-send-escape
        :i "j" #'my-vterm-j-set-k-as-switch-to-normal-state))

Vim 的小毛病太多了,j k 转 esc 都有人专门写插件优化……

促使我转 Emacs 的第一个原因就是 Vim 记录按键历史的方法很脏很傻,不像 Emacs 插件 keyfreq 短短五六百行代码实现了很多功能。

原来还能删掉再esc来避免等它!那楼主的需求就也可以这么实现

对的, 把我这个纸糊的函数稍微改一下就可以实现递归映射了。

没看明白怎么实现,我的理解是 Emacs key sequence 没有 timeout 只有 prefix,而 C-c d 不能在作为 C-c d d 的 prefix 的同时又是一个单独的 key sequence。

我想了一下,你的这个需求其实和我遇到的场景确实不太一样,我的场景是第一件事情一定会做,然后在一段时间内做第二件事情,要不然就不做这件事情。

你的事情是第一件事情在一段时间内不做,过一段时间后啥事情都不干就去做。

我想了一个非常 hack 的办法,非常丑陋 (标准屎山代码)。

my-func1 是你想要做的第一件事情,my-func2 是你想要做的第二件事情。然后把你的第一个按键绑定为 这样的一个函数,先设定 D 为你要做的第二件事情的包裹,然后再设定一个计时器,计时器到了就会执行你做的第一件事情并且把D给取消绑定。然后第二件事情的包裹是取消掉这个计时器 (这样第一件事情就不会被执行),并且把D给取消绑定,再做第二件事情。

(defun my-func1 ()
    (interactive)
    (message "func1"))

(defun my-func2 ()
    (interactive)
    (message "func2"))

(defun my-func2-wrap ()
    (interactive)
    (cancel-timer my-temp-timer)
    (general-define-key
     :states 'normal
     "D" #'evil-delete-line)

    (funcall #'my-func2))

(defun my/do-two-things-first-step ()
    (interactive)
    (general-define-key
     :states 'normal
     "D" #'my-func2-wrap)
    
    (setq my-temp-timer
          (run-with-idle-timer 0.4 nil
                               (lambda ()
                                   (my-func1)
                                   (general-define-key
                                    :states 'normal
                                    "D" #'evil-delete-line))))
    )

(general-define-key
 :states 'normal
 "C-c d" #'my/do-two-things-first-step)


更新一波,可以用 key-binding 函数查询得到按键对应的函数。这样取消绑定设回 fallback 命令的时候就不需要 hard-coding 原来的命令了。

1 个赞