在上一篇,我们了解了如何通过 interactive 构建交互式函数。Emacs中可以说所有用户的操作都是靠这类函数完成,就连基本的插入删除,我们虽然是通过键鼠在UI上操作,但是它的底层仍然是依赖这类交互式函数。我们不可能每个操作都要依靠M-x 执行交互式函数,因此Emacs提供了快捷键绑定的方式,提升效率
背景知识介绍
在 Emacs 中,按键并不是直接映射给函数的,而是通过 Keymap(按键映射表) 进行中转。keymap本质上是一个将“事件类型”映射到“命令”或其他keymap的查找表。
下面我们来探讨一下,从按下快捷键到最终执行对应的交互式函数,这中间发生了什么。
首先我们按下快捷键,Emacs 通过 read-key-sequence 拿到了这个快捷键。然后它会按照以下顺序查找按键:
overriding-terminal-local-mapoverriding-local-map文本属性键映射: 如果按键发生在特定文本上,会查找该文本的keymap或local-map属性emulation-mode-map-alists: 用于仿真其他编辑器的键映射minor-mode-overriding-map-alist: 用于覆盖 minor mode 键映射minor-mode-map-alist: 当前激活的 minor mode 键映射当前Buffer的本地键映射: 由当前 Major Mode 定义全局键映射: 全局生效的键映射
如果找到的是一个keymap对象,也就是说这个键其实绑定了另一个keymap对象而不是一个函数,那么此时它会被视为一个前缀。如果后面跟了一个输入,则按照输入从当前前缀绑定的keymap中继续往下找,直到遍历完整个链。如果没有输入它会停在那里等待用户的进一步输入。
我们建立快捷键映射实际就是在往这些表中插入一条记录。Emacs提供了通用的函数 define-key 和 keymap-set 来操作map记录。
(define-key KEYMAP KEY DEF &optional REMOVE)keymap对应着上面的一个keymapkey为快捷键def对应着绑定的函数
上述几类keymap,我们在使用 define-key 插入时通常有固定的模式。
- 直接使用变量名的
这类主要是global-map,例如(define-key global-map (kbd "C-c a") 'my-global-command) - 对应具体 Mode的
每个Mode 都有一个类似mode-map的变量,例如 python-mode 中有一个python-mode-map、elisp 中有一个elisp-mode-map,这种情况,我们可以使用(define-key elisp-mode-map (kbd "C-c e") 'my-elisp-mode-command)
但是对于minor mode 来说在绑定前要确保对应的 map 存在, 可以通过查找当前名称是否在 minor-mode-map-alist 当中来判断。并且需要保证 minor mode 处于激活状态
- 需要通过 (current-local-map) 获取的
对于当前buffer的本地映射来说,我们需要使用 current-local-map 来获取当前buffer中的快捷键绑定map,后续通过这个map来进行绑定 (define-key (current-local-map) (kbd "C-c c") 'my-current-command)
- 需要先创建再激活的
对于 overriding-local-map 和 overriding-terminal-local-map 来说,它们默认的值是 nil 我们必须将它指向具体的keymap对象,然后进行操作.
;; 1. 创建一个临时的 keymap
(setq my-temp-map (make-sparse-keymap))
;; 2. 向这个临时 map 里添加绑定
(define-key my-temp-map (kbd "C-c e") 'my-emergency-command)
;; 3. 激活它(注意:这会瞬间劫持所有键盘输入!)
(setq overriding-local-map my-temp-map)
;; 如果要取消劫持:
(setq overriding-local-map nil)- 无法使用
define-key操作
对于 文本属性键映射 来说,它本质上是一个文本属性,并没有对应具体的keymap 对象,这种情况下,可以通过之前介绍过的 put-text-property 设置一个keymap对象
;; 让缓冲区第 1 到第 10 个字符拥有独立的按键映射
(let ((my-text-map (make-sparse-keymap)))
(define-key my-text-map (kbd "RET") 'my-special-enter)
(put-text-property 1 10 'keymap my-text-map))define-key 是一个使用C编写的非常底层的函数了,从上面可以看到,每种类型的keymap 在传入它的keymap 参数时都有不同的处理手段。为了简便Emacs还提供了一些函数:
global-set-key相当于keymap参数传入了global-maplocal-set-key相当于keymap参数传入了current-local-map
而且从 Emacs(29) 开始引入了更为简便的新的绑定快捷键的函数。keymap-set 用于取代 define-key、keymap-global-set 用于取代 globak-set-key、keymap-local-set 用于取代 local-set-key
它们与原来的函数相比,直观上看最大的区别就是不再需要使用 kdb 宏包裹,而是支持直接使用字符串来表示快捷键。例如可以写类似 (keymap-set (current-local-map) "C-c c" 'my-current-command)。Emacs底层并不接受这种用字符串包装的快捷键,它接受一个特殊结构的向量,而kbd就是一个将字符串转成对应这个结构的转换器。
对于已经设置上的快捷键我们可以使用 keymap-unset 或者 define-key 将对应的 def 参数设置为 nil
我们可以查看函数绑定的快捷键以及快捷键绑定的函数。我们可以使用 describe-key 查看某个快捷键绑定到了哪个函数上,也可以使用 describe-function 看到对应函数绑定的快捷键
如果我们希望通过elisp编程的方式实现上述内容,我们可以使用如下的一些函数:
(key-binding KEY)获取当前所有活动键映射中,某个按键序列 KEY 所绑定的命令(lookup-key KEYMAP KEY)在指定的 keymap 中查找key 的绑定(where-is-internal COMMAND KEYMAP)查找对应命令在 keymap 中的绑定
练习
前面我们实现了很多常见的小功能,如果读者觉得某些功能有用,可以考虑利用今天所学的绑定快捷键的方式来为那些函数绑定一个快捷键以提升效率。本节的练习我不打算进行添加一个快捷键绑定就完事了。
本节我们利用这些查找函数完成两个小功能,查找当前快捷键绑定的函数以及查找函数所绑定的快捷键。
(defun my/inspect-key-to-function(key)
(interactive "skeymap:")
(let* ((key-seq (kbd key)) ;;这里需要进行一个字符串到Emacs认可格式的转化
(cmd (key-binding key-seq)))
(if cmd
(message "快捷键[%s]绑定到 --> [%s]"))
(message "未找到当前快捷键绑定的函数")))
(defun my/inspect-function-to-key(command)
(interactive (list (read-command "Input Command:")))
(let ((keybinds (where-is-internal command)))
(if keybinds
(let ((read-keys (mapcar #'key-description keybinds)))
(message "find binds: %s" (mapconcat (lambda (k) (format "[%s]" k)) read-keys " "))))))上述代码都是很基础的函数调用。可能有多个快捷键绑定到同一个函数上,所以 where-is-internal 实际返回的是一个列表。我们通过 mapcar 对列表中每一个成员进行迭代,并通过 key-description 获取对应的快捷键。最终我们利用 mapconcat 将前一步返回的 read-keys 做一个迭代,拼接成一个字符串进行显示
总结
本篇我们介绍了快捷键绑定的基础内容,并介绍了Emacs中的多层keymap,以及操作这些keymap的一些方法。下面是今天介绍的api函数:
define-keykeymap-setglobal-set-keylocal-set-keykeymap-global-setkeymap-local-setkeymap-unsetkey-bindingwhere-is-internal
评论 (0)