Elisp: Conditionally change keybinding

2020-02-05 09:56发布

I'm trying to write a custom tab completion implementation which tries a bunch of different completions depending on where the point is. However, if none of the conditions for completions are met I would like tab to do what ever the current mode originally intended it to do.

Something like this:

(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?

Currently I'm checking for specific modes and doing the right thing for that mode, but I really would like a solution that just does the right thing without me having to explicitly add a condition for that specific mode.

Any ideas of how to do this?

Thanks! /Erik

标签: emacs elisp
5条回答
小情绪 Triste *
2楼-- · 2020-02-05 10:05

BTW, here is another solution:

(define-key <map> <key>
  `(menu-item "" <my-cmd> :filter ,(lambda (cmd) (if <my-predicate> cmd))))
查看更多
在下西门庆
3楼-- · 2020-02-05 10:08

Here is a macro I wrote based on Emacs key binding fallback to define a keybinding conditionally. It adds the keybinding to the specified minor mode but if the condition is not true, the previously assigned action is executed:

(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))))))

Then I can do things like the following to use the special binding for TAB only when I am on a header in outline-minor-mode. Otherwise my default action (I have both indent and yasnippets) is executed:

(define-key-with-fallback outline-minor-mode-map (kbd "TAB") 
  (outline-cycle 1) (outline-on-heading-p))
查看更多
够拽才男人
4楼-- · 2020-02-05 10:09

It's possible that you could achieve this without any special workarounds at all. In most modes TAB just does indentation by default, but if you set the global variable tab-always-indent to 'complete it will try to do completion first, and indent if no completion is possible. This usually works really well, although if TAB is bound to another command in one of your major modes you might be out of luck.

If that works in the modes you need, you'll just need to add your custom completion function to the front of the list completion-at-point-functions in all applicable buffers (maybe using a mode hook). The completion-at-point command calls each function listed in completion-at-point-functions until one of them returns non-nil, so all you need to do to have your custom completion function "fall through" to the existing behavior is return nil from it.

This isn't a 100% answer to the question, but if the major modes you're working with are written according to the normal guidelines it might be the cleanest way.

查看更多
干净又极端
5楼-- · 2020-02-05 10:28

You could use functions such as key-binding (or its more specific variants global-key-binding, minor-mode-key-binding and local-key-binding) to probe active keymaps for bindings.

For example:

(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

One way to avoid infinite loops if your command is bound to TAB could be to put your binding in a minor mode, and temporarily disable its keymap while looking for the TAB binding:

(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"))))))
查看更多
神经病院院长
6楼-- · 2020-02-05 10:29

define-key can accept quoted string or interactive lambdas like in this example.

;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's can be replaced with named functions like my-tab-completion and used more effectively.

From define-key's docstring (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'.)
查看更多
登录 后发表回答