Elisp: 条件性更改按键绑定

6
我正在尝试编写一个自定义的选项卡完成实现,它根据当前光标所在位置尝试许多不同的选项。然而,如果没有满足选项完成条件的情况,我希望选项卡执行当前模式最初意图执行的操作。
类似这样:
(defun my-custom-tab-completion ()
  (interactive)
  (cond
   (some-condition
    (do-something))
   (some-other-condition
    (do-something-else))
   (t
    (do-whatever-tab-is-supposed-to-do-in-the-current-mode))) ;; How do I do this?

目前我正在检查特定的模式并为该模式执行正确的操作,但我真的希望有一种解决方案,可以在不需要显式添加该特定模式条件的情况下自动执行正确的操作。

有什么想法吗?

谢谢!/Erik


1
请查看 define-keylocal-set-key 的文档。通常通过修改特定模式的键映射来完成此操作。 - Miserable Variable
5个回答

6
顺便提一下,这里还有另一个解决方案:
(define-key <map> <key>
  `(menu-item "" <my-cmd> :filter ,(lambda (cmd) (if <my-predicate> cmd))))

你能举个例子来说明如何使用它吗?例如,<map> 应该是什么?而 menu-item 是什么 / 为什么在这里是必要的? - ShreevatsaR
@ShreevatsaR: <map> 应该是你想要添加(条件)绑定的任何地方的映射。例如,你的主模式的映射。menu-item 只是一个特殊的符号,需要放在我放置它的位置才能使其工作。这个 "menu-item + :filter" 最初是为了能够动态构建特定的(子)菜单(例如包含缓冲区和帧列表的菜单)而添加的,因此得名;但由于菜单使用键映射实现,所以它也适用于非菜单元素。 - Stefan
谢谢!那很有帮助,对我来说几乎可以工作了。我做了这样的事情:(add-hook 'gfm-mode-hook'(lambda()(progn(define-key gfm-mode-map(kbd“`”)`(menu-item“”markdown-insert-code:filter,(lambda(cmd)(if mark-active cmd)))))))) 我看到的问题是,当谓词不为真时,键的含义似乎是拾取全局键绑定(在这种情况下为self-insert-command),而不是特定于模式的键绑定。 - ShreevatsaR
@ShreevatsaR:[请不要引用您的lambda表达式!]确实,您所做的是使用条件绑定覆盖了特定于模式的绑定,要使用这种技术,您需要将条件绑定添加到其他某个映射中。但在您的情况下,这可能不是我推荐的技术。 - Stefan
你有什么建议?(据我所知,我的情况恰好是这个问题的一个特例:我想学习一种通用技术,在给定模式下,将一个键绑定到自定义函数(如果某个谓词为真),否则绑定到该模式的默认键绑定。不幸的是,我很难理解你试图解释的解决方案 :-) 例如,应该将键绑定添加到模式映射中还是不添加?) - ShreevatsaR

4

这是我基于Emacs键绑定回退编写的宏,用于有条件地定义键绑定。它将键绑定添加到指定的次要模式中,但如果条件不成立,则执行先前分配的操作:

(defmacro define-key-with-fallback (keymap key def condition &optional mode)
  "Define key with fallback. Binds KEY to definition DEF in keymap KEYMAP, 
   the binding is active when the CONDITION is true. Otherwise turns MODE off 
   and re-enables previous definition for KEY. If MODE is nil, tries to recover 
   it by stripping off \"-map\" from KEYMAP name."
  `(define-key ,keymap ,key
     (lambda () (interactive)
        (if ,condition ,def
          (let* ((,(if mode mode
                     (let* ((keymap-str (symbol-name keymap))
                            (mode-name-end (- (string-width keymap-str) 4)))
                       (if (string= "-map" (substring keymap-str mode-name-end))
                           (intern (substring keymap-str 0 mode-name-end))
                         (error "Could not deduce mode name from keymap name (\"-map\" missing?)")))) 
                  nil)
                 (original-func (key-binding ,key)))
            (call-interactively original-func))))))

在使用轮廓模式时,当我处于标题上时,我可以像下面这样执行特殊的TAB绑定操作。否则,将执行我的默认操作(我同时具有缩进和yasnippets):

(define-key-with-fallback outline-minor-mode-map (kbd "TAB") 
  (outline-cycle 1) (outline-on-heading-p))

3
您可以使用函数(如key-binding,或其更具体的变体global-key-bindingminor-mode-key-bindinglocal-key-binding)来探查活动按键映射的绑定。

例如:

(call-interactively (key-binding (kbd "TAB")))
;; in an emacs-lisp-mode buffer:
;;    --> indent-for-tab-command
;; 
;; in a c++-mode buffer with yas/minor-mode:
;;    --> yas/expand

一种避免无限循环的方法是将命令绑定到 TAB,然后将其绑定到次要模式中,并在查找 TAB 绑定时暂时禁用其按键映射:

(define-minor-mode my-complete-mode
  "Smart completion"
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map (kbd "TAB") 'my-complete)
            map))

(defun my-complete ()
  (interactive)
  (if (my-condition)
      (message "my-complete")
    (let ((my-complete-mode nil))
      (call-interactively (key-binding (kbd "TAB"))))))

谢谢你的回答!问题在于我想将命令绑定到Tab键上,所以按键绑定实际上会返回函数本身,从而导致无限循环。我能否在某种程度上查找“原始”模式下的键映射中的绑定? - Erik Öjebo
你说得对,这并不容易实现。请看我的编辑,可能有一个解决方法。 - François Févotte
对于次要模式,您不需要从minor-mode-map-alist中删除条目:只需将次要模式变量(又名my-complete-mode)绑定到对key-binding的调用周围即可。 - Stefan
@Stefan,你能否发布一段代码片段,展示一下let绑定可能是什么样子,以避免删除条目? - Erik Öjebo
@Stefan 不错的想法,谢谢。我编辑了我的答案以考虑这一点。 - François Févotte
谢谢!很好,我也得学会写自己的小模式 :) - Erik Öjebo

2
在大多数模式下,TAB键默认只进行缩进,不需要任何特殊的解决方法即可实现此功能。但是,如果将全局变量tab-always-indent设置为'complete,它将首先尝试完成操作,如果无法完成,则进行缩进。这通常非常有效,但如果TAB在其中一个主要模式中绑定到另一个命令,则可能会失败。
如果在所需模式下起作用,则只需将自定义完成函数添加到所有适用缓冲区的列表completion-at-point-functions的开头(可以使用模式挂钩)。completion-at-point命令调用completion-at-point-functions中列出的每个函数,直到其中一个返回非nil值,因此您只需要从其中返回nil即可使自定义完成函数“落入”到现有行为中。
虽然这不是对问题的100%答案,但如果您使用的主要模式按照正常指南编写,则可能是最清洁的方法。

这是一个非常好的观点,但很多小模式可以重新绑定TAB键,例如YASnippet或自动完成。您知道这样的小模式是否尊重(setq tab-always-indent 'complete)吗? - François Févotte
如果你想要覆盖这些次要模式,那就去做吧。你仍然可以使用indent-for-tab-command。顺便说一句,如果tab-always-indentcomplete值与yasnippet不兼容,你可能想把这个问题报告为一个bug(在yasnippet中)。 - Stefan

1

define-key可以接受带引号的字符串或交互式lambda表达式,就像这个例子一样。

;Static
(define-key evil-normal-state-mapr "m" 'evil-motion-state)
;Conditional
(define-key evil-normal-state-map "m" 
  (lambda () (interactive) (message "%s" major-mode)))

Lambda表达式可以被替换为像my-tab-completion这样的命名函数,从而更有效地使用。
来自define-key的文档字符串(Emacs 25)
DEF is anything that can be a key's definition:
 nil (means key is undefined in this keymap),
 a command (a Lisp function suitable for interactive calling),
 a string (treated as a keyboard macro),
 a keymap (to define a prefix key),
 a symbol (when the key is looked up, the symbol will stand for its
    function definition, which should at that time be one of the above,
    or another symbol whose function definition is used, etc.),
 a cons (STRING . DEFN), meaning that DEFN is the definition
    (DEFN should be a valid definition in its own right),
 or a cons (MAP . CHAR), meaning use definition of CHAR in keymap MAP,
 or an extended menu item definition.
 (See info node `(elisp)Extended Menu Items'.)

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