Run function before keymap commands

223 views Asked by At

Is there a way to advise a keymap or otherwise run a function after a keymap prefix, but before commands in the keymap?

Say I have a keymap with bindings for hideshow, but these bindings are only useful after hs-minor-mode is activated. How can I run (hs-minor-mode) after the prefix is entered, but before the functions in the map are called?

I thought making a prefix command and advising it might work, but that is an error (below).

Example:

(let ((map (define-prefix-command 'my-activate-fold 'my-fold-map)))
  (define-key map "a" #'hs-hide-all)
  (define-key map "s" #'hs-show-all)
  (define-key map "l" #'hs-hide-level)
  (global-set-key (kbd "<f6>") 'my-activate-fold))

;; error: wrong-type-argument commandp my-activate-fold
(define-advice my-activate-fold (:before (fn &rest r) "activate-hideshow")
  (hs-minor-mode)
  (apply fn r))
2

There are 2 answers

4
Tim X On

If I understand correctly, I think you may be approaching this incorrectly. For your specific question, no, there is no way to define a function which will run after a prefix key is called but before the command (an interactive function) which is bound to the key is run. However, I'm not sure that is really what your after. You can of course define commands which can wrap another command and do whatever you want. However, I'm not sure that is what you want either. You might want to state exactly what you want rather than part of what you believe is the solution to what you want.

Normal practice is for a minor mode to define a keymap and you add your mode specific key bindings to that map in a mode initialisation hook. In the case of hs-minor-mode, that is called hs-minor-mode-map. This map only exists inside buffers running hs-minro-mode and it takes precedence over the global map. So this is where you want to place your mode specific bindings. To do this, create a function which adds the bindings to the map and attach that to the hs-minor-mode-hook, which is run when hs-minor-mode is loaded.

(add-hook 'hs-minor-mode-hook (lambda ()
                                (define-key 'hs-minor-mode-map "a" #'hs-hide-all)
                                ...))

The key bindings defined above will only exist if hs-minor-mode is active in the buffer. If it is not active, the keys used in the binding will either be bound to whatever the next highest map in the mode is or the global map or nothing (see the section on keymaps in the elsip manual for full details).

If what you want is to have specific hs-minor-mode bindings only exist in specific modes, then you can define those bindings in the keymap for that mode. For example, if you wanted hs-minor-mode bindings that only exist when you run hs-minor-mode in js2-mode, but not when you run hs-minor-mode in c-mode, then you can add the bindings to the js2-mode-map and load hs-minor-mode as part of the js2-mode-hook

If it is something else you are after, you need to clarify.

0
phils On

FWIW, you could simply advise the commands themselves:

(defun my-ensure-hs-minor-mode (&rest _args)
  "Ensure `hs-minor-mode' is active."
  (unless (bound-and-true-p hs-minor-mode)
    (hs-minor-mode 1)))

(advice-add 'hs-hide-all :before #'my-ensure-hs-minor-mode)
(advice-add 'hs-show-all :before #'my-ensure-hs-minor-mode)
(advice-add 'hs-hide-level :before #'my-ensure-hs-minor-mode)

Your keymap then just works:

(require 'hideshow)

(let ((map (define-prefix-command 'my-activate-fold 'my-fold-map)))
  (define-key map "a" #'hs-hide-all)
  (define-key map "s" #'hs-show-all)
  (define-key map "l" #'hs-hide-level))

(global-set-key (kbd "<f6>") 'my-activate-fold)