Emacs如何将命令绑定到所有按键,但保留少数按键?

5

我想创建一个次要模式(foo-mode),它有自己的键映射(foo-mode-map),但是当用户按下不在(foo-mode-map)中的任何键时,此次要模式应该退出。 我如何绑定 turn-off-foo-mode 到所有其他键?

编辑:这是我根据所选择的答案想出的解决方案。 它也接受数字输入。

(defalias 'foo-electric-delete 'backward-kill-word)

(defun foo-mode-quit (&optional arg)
  (interactive)
  (let ((global-binding (lookup-key (current-global-map)
                                    (single-key-description last-input-event))))
    (unless (eq last-input-event ?\C-g)
      (push last-input-event unread-command-events))
    (unless (memq global-binding
                  '(negative-argument digit-argument))
      (foo-mode -1))))

(defvar foo-mode-map
  (let ((map (make-keymap)))
    (set-char-table-range (nth 1 map) t 'foo-mode-quit)
    (define-key map "-" 'negative-argument)
    (dolist (k (number-sequence ?0 ?9))
      (define-key map (char-to-string k) 'digit-argument))
    (define-key map [backspace] 'foo-electric-delete)
    map))

(define-minor-mode foo-mode
  "Toggle Foo mode.
     With no argument, this command toggles the mode.
     Non-null prefix argument turns on the mode.
     Null prefix argument turns off the mode.

     When Foo mode is enabled, the control delete key
     gobbles all preceding whitespace except the last.
     See the command \\[foo-electric-delete]."
  ;; The initial value.
  :init-value nil
  ;; The indicator for the mode line.
  :lighter " Foo"
  ;; The minor mode bindings.
  :keymap foo-mode-map
  :group 'foo)
4个回答

4

您需要决定的一件事情是,当用户按下另一个键时,您是否应该简单地退出您的模式,或者您是否应该运行与所问键绑定的命令(例如在增量搜索中)。

一种方法是使用pre-command-hook来检查this-command是否是您的命令之一,并在不是的情况下关闭您的模式。


使用 pre-command-hook 对于每个按键来说都是很大的开销;当你按下一个键并按住它时,这非常明显...重复会变得非常缓慢。此外,使用 self-insert-command 也可能会出现问题。 - aculich
减速问题可以通过在退出 foo-mode 时移除挂钩来轻松处理。诚然,在 foo-mode 运行时会有一些减速,但我怀疑这并不会产生影响,除非它是一个非常时间关键的包(我非常怀疑)。 - Lindydancer
关于您声称这不应该与self-insert-command一起工作的说法,您能详细说明一下吗?(我刚刚测试了一下,它似乎完全正常...) - Lindydancer
如果j被绑定到next-line以模拟vi中的导航,那么减速将成为一个问题的例子是;当使用pre-command-hook时,速度会有明显差异。 - aculich

4
解决方案2:您可以使用set-char-table-range设置映射表中的所有字符。例如:
(defvar foo-mode-map
  (let ((map (make-keymap)))
    (set-char-table-range (nth 1 map) t 'foo-turn-off-foo-mode)
    ...
    map))

除非您在“let”中将“map”作为最后一个表达式添加,否则此代码将无法正常工作。否则,“foo-mode-map”将绑定到最后一个表达式的值;在您的示例中,这将是由“set-char-table-range”返回的值或者他们填充的任何内容。 - aculich

4

我已经包含了一个完整的工作示例,用于创建一个带有所需行为的小模式;关键是使用make-keymap创建的按键映射中的set-char-table-range,该方法创建了一个密集的按键映射,具有完整的char-table;在使用make-sparse-keymap创建的稀疏按键映射上运行该方法将不起作用。

(defalias 'foo-electric-delete 'backward-kill-word)

(defun foo-mode-quit (&optional arg)
  (interactive)
  (foo-mode -1))

(defvar foo-mode-map
  (let (map (make-keymap))
    (set-char-table-range (nth 1 map) t 'foo-mode-quit)
    (define-key map [backspace] 'foo-electric-delete)
    map))

(define-minor-mode foo-mode
  "Toggle Foo mode.
     With no argument, this command toggles the mode.
     Non-null prefix argument turns on the mode.
     Null prefix argument turns off the mode.

     When Foo mode is enabled, the control delete key
     gobbles all preceding whitespace except the last.
     See the command \\[foo-electric-delete]."
  ;; The initial value.
  :init-value nil
  ;; The indicator for the mode line.
  :lighter " Foo"
  ;; The minor mode bindings.
  :keymap foo-mode-map
  :group 'foo)

(defvar major-baz-mode-map '(keymap (t . major-baz-mode-default-function)))

为一个主模式映射设置默认绑定更容易,我在这里提供一个示例,但正如我上面所指出的那样,这种简约键位映射不适用于次要模式:

(defvar major-baz-mode-map '(keymap (t . major-baz-mode-default-function)))

这在键位图格式的文档中有讨论,其中写道:

(t . binding)

This specifies a default key binding; any event not bound by other
elements of the keymap is given binding as its binding. Default
bindings allow a keymap to bind all possible event types without
having to enumerate all of them. A keymap that has a default binding
completely masks any lower-precedence keymap, except for events
explicitly bound to nil (see below).

我必须发出一个关于上面给出的示例的重大警告。在实现Emacs包时,能够重新加载模块而不覆盖用户设置非常重要。通常,如果变量已经有值,defvar不会覆盖它。上面的代码将defvar与像set-char-table-range这样的显式调用修改函数混合了起来。 - Lindydancer
@Lindydancer 谢谢,你说得对。我更新了代码,使用了 let - aculich
你在 (let ...) 的第一个参数中丢失了一对括号。 - kuanyui

1

答案3:

我认为所选的解决方案过于复杂。开始操纵事件队列并不是你通常想做的事情。

相反,我建议采用完全不同的解决方案。如果你始终保持模式开启,你可以将像退格这样的键绑定到一个函数上,该函数可以自行确定它应该按字符还是按单词操作。

下面是一个简单的概念验证。它仍然有一个明确的函数进入单词模式,但是它可以被概括为既启用单词模式又执行某种操作的函数。

(defun foo-electric-enabled nil
  "Non-nil when foo-mode operate in word mode.")

(defun foo-electric-delete (&optional arg)
  (interactive "p")
  (if (and foo-electric-enabled
           (memq last-command '(kill-region
                                foo-electric-delete
                                foo-enable-word-operations)))
      (call-interactively 'backward-kill-word)
    (setq foo-electric-enabled nil)
    (call-interactively 'backward-delete-char-untabify)))

(defvar foo-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map [backspace] 'foo-electric-delete)
    map))

(defun foo-enable-word-operations ()
  (interactive)
  (setq foo-electric-enabled t))

(define-minor-mode foo-mode
  "Toggle Foo mode.
With no argument, this command toggles the mode.
Non-null prefix argument turns on the mode.
Null prefix argument turns off the mode.

When Foo mode is enabled, the backspace key
gobbles all preceding whitespace except the last.
See the command \\[foo-electric-delete]."
  ;; The initial value.
  :init-value nil
  ;; The indicator for the mode line.
  :lighter " Foo"
  ;; The minor mode bindings.
  :keymap foo-mode-map
  :group 'foo)

上面的代码可以进行改进,但我将其留给读者作为练习:

  • 不必检查函数是否是文字列表的成员,可以使用模块范围内的列表,或者您可以在应该保持单词模式的函数上使用属性。

  • 像模式行指示器这样的东西总是开着的。通过将此包实现为使用另一个较小的模式,您可以使用第二个来拥有模式行指示器,并指示该包处于单词模式,替换foo-electric-enabledfoo-enable-word-operations

  • 函数foo-electric-delete明确调用backward-kill-wordbackword-delete-char-untabify。有许多技术可以分别调用绑定到meta backspacebackspace的任何内容。


这类似于“post-command-hook”解决方案,但更加复杂? ;) 实际上,我在我的使用中选择了一个“post-command-hook”。 - event_jr
我认为这与“post-command-hook”解决方案非常不同。首先,“post-command-hook”在命令之后运行,因此您无法以任何方式更改命令的操作。其次,每个添加到“post-”或“pre-command-hook”的内容都会受到惩罚,因为您发出的每个命令都必须通过您的钩子传递。此外,添加到此类钩子的函数无法发出错误(这样做时,它们将被静默地从钩子中删除)。20年的Emacs开发经验告诉我要远离“pre-”和“post-command-hook”,除非我确实需要使用它们。 - Lindydancer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接