5

A long time ago I implemented remove-key to completely undo the effect of define-key, but when I just tried that implementation, it did not work reliably. How would you implement remove-key so that for any given key this is true:

(let ((map (make-sparse-keymap)))
  (define-key map KEY 'bound)
  (remove-key map KEY)
  map)
=> (keymap)

It should also work for non-sparse keymaps. And when a sub-keymap is involved which becomes empty when the key is removed, then the sub-keymap should also be removed.

Here's the documentation - now I only need an implementation :-)

(defun remove-key (keymap key)
  "In KEYMAP, remove key sequence KEY.
Make the event KEY truely undefined in KEYMAP by removing the
respective element of KEYMAP (or a sub-keymap) as opposed to
merely setting it's binding to nil.

There are several ways in which a key can be \"undefined\":

   (keymap (65 . undefined) ; A
           (66))            ; B

As far as key lookup is concerned A isn't undefined at all, it is
bound to the command `undefined' (which doesn't do anything but
make some noise).  This can be used to override lower-precedence
keymaps.

B's binding is nil which doesn't constitute a definition but does
take precedence over a default binding or a binding in the parent
keymap.  On the other hand, a binding of nil does _not_ override
lower-precedence keymaps; thus, if the local map gives a binding
of nil, Emacs uses the binding from the global map.

All other events are truly undefined in KEYMAP.

Note that in a full keymap all characters without modifiers are
always bound to something, the closest these events can get to
being undefined is being bound to nil like B above."
  ...)

Why do I want that? It has already been mentioned in the above doc-string, but here it is again, quoting (elisp)Format of Keymaps (emphasis mine).

When the binding is nil, it doesn't constitute a definition but it does take precedence over a default binding or a binding in the parent keymap. On the other hand, a binding of nil does not override lower-precedence keymaps; thus, if the local map gives a binding of nil, Emacs uses the binding from the global map.

(define-key map KEY nil) does not remove the entry for KEY, it just sets the binding to nil, which "doesn't constitute a definition", but which never-the-less can have an effect.

4
  • A general implementation that does what you want in all possible cases is a fair bit of work. So I think you should give a bit more detail about the particular situation where you need it: the fact that this operation doesn't exist yet is a good hint that it's rarely needed.
    – Stefan
    Commented Oct 17, 2015 at 17:12
  • @Stefan: it's probably rarely needed because default key bindings are rarely used (I guess the question arose from a magit related situation since tarsius is magit's maintainer, and magit does use default key bindings).
    – npostavs
    Commented Oct 17, 2015 at 19:15
  • @npostavs No this has nothing to do with Magit. I would just like to edit existing keymaps in a semi-automated way following a set of rules of the form "if X is bound to a command then instead bind Y to that command and unbind X". It's more complicated than that, but I hope you get the idea.
    – tarsius
    Commented Oct 17, 2015 at 20:01
  • @Stefan "you should give a bit more detail about the particular situation where you need it". Yes, I will ask a separate question for that.
    – tarsius
    Commented Oct 17, 2015 at 20:01

3 Answers 3

3

Eventually I came up with a solution that satisfies me and added it to my keymap-utils package.

(defun kmu-remove-key (keymap key)
  "In KEYMAP, remove key sequence KEY.
Make the event KEY truely undefined in KEYMAP by removing the
respective element of KEYMAP (or a sub-keymap) as opposed to
merely setting its binding to nil.

There are several ways in which a key can be \"undefined\":

   (keymap (65 . undefined) ; A
           (66))            ; B

As far as key lookup is concerned A isn't undefined at all, it is
bound to the command `undefined' (which doesn't do anything but
make some noise).  This can be used to override lower-precedence
keymaps.

B's binding is nil which doesn't constitute a definition but does
take precedence over a default binding or a binding in the parent
keymap.  On the other hand, a binding of nil does _not_ override
lower-precedence keymaps; thus, if the local map gives a binding
of nil, Emacs uses the binding from the global map.

All other events are truly undefined in KEYMAP.

Note that in a full keymap all characters without modifiers are
always bound to something, the closest these events can get to
being undefined is being bound to nil like B above."
  (when (stringp key)
    (setq key (kmu-parse-key-description key t)))
  (define-key keymap key nil)
  (setq key (cl-mapcan (lambda (k)
                         (if (and (integerp k)
                                  (/= (logand k ?\M-\0) 0))
                             (list ?\e (- k ?\M-\0))
                           (list k)))
                       key))
  (if (= (length key) 1)
      (delete key keymap)
    (let* ((prefix (vconcat (butlast key)))
           (submap (lookup-key keymap prefix)))
      (when (and (not (eq submap 'ESC-prefix))
                 (= (length submap) 1))
        (delete (last key) submap)
        (kmu-remove-key keymap prefix)))))

The old answer because it discusses some details:

After looking at it again I realized that my existing implementation actually works quite well, but is very picky about the format of the KEY. It has to be a vector in the "internal format", so [?\C-i] would work but [(control ?i)] would not. All elements have to be either integers or symbols.

(defun remove-key (keymap key)
  (define-key keymap key nil)
  (setq key (cl-mapcan (lambda (k)
                         (if (and (integerp k)
                                  (/= (logand k ?\M-\^@) 0))
                             (list ?\e (- k ?\M-\^@))
                           (list k)))
                       key))
  (if (= (length key) 1)
      (delete key keymap)
    (let* ((prefix (vconcat (butlast key)))
           (submap (lookup-key keymap prefix)))
      (delete (last key) submap)
      (when (= (length submap) 1)
        (remove-key keymap prefix)))))

?\M-\^@ is Emacs' value for the meta key.

To make this function less picky about the format of the KEY, this might work:

(setq key (if (stringp key)
              (edmacro-parse-keys key t)
            (edmacro-parse-keys (key-description key) t)))

The first line (define-key keymap key nil) should take care of the case where KEY is part of the full keymap's char-table, assuming this is correct:

Note that in a full keymap all characters without modifiers are always bound to something, the closest these events can get to being undefined is being bound to nil like B above.

That first line should also prevent the parent keymap from being modified, in the non-char-table case. I do not avoid doing that for a full keymap, because there is no full-keymap-p predicate. Usually the first element of a full keymap is the char-table, but last I checked that was not actually enforced, a keymap also functions as a full keymap if a later element is a char-table. It just seemed wiser to do some potentially unnecessary work instead of risking getting it wrong.

2
  • 1
    ?\M-\^@ (or just ?\M-\0) gives Emacs' character value for the meta key, so the cl-mapcan is converting any M-<x> into ESC <x> (and also converting vector to list).
    – npostavs
    Commented Oct 17, 2015 at 20:33
  • I've accepted my own answer so that the @Community bot can relax. But I am still open for other suggestions.
    – tarsius
    Commented Jan 15, 2016 at 22:54
2

You could start with

(defun remove-key (keymap key)
  (let ((binding (assq key keymap))
        (parentbinding (assq key (keymap-parent keymap))))
    (and binding
         (not (eq binding parentbinding)) ;Don't change the parent!
         (delq binding keymap))))

This will only work for a sparse keymap and expects key to be a single key, rather than a key sequence. [ And it's guaranteed 100% untested, so even for that limited case it might not work. ]

2

This is a simplified version of information extracted from the accepted answer posted by tarsius which contains more advanced information and requires a package for kmu-parse-key-description. This small stand alone function undoes the effect of define-key by removing the declaration from the keymap. Removing the declaration is more effective than setting its value to nil.

(defun remove-key (keymap key)
  (define-key keymap key nil)
  (setq key (cl-mapcan (lambda (k)
                         (if (and (integerp k)
                                  (/= (logand k ?\M-\^@) 0))
                             (list ?\e (- k ?\M-\^@))
                           (list k)))
                       key))
  (if (= (length key) 1)
      (delete key keymap)
    (let* ((prefix (vconcat (butlast key)))
           (submap (lookup-key keymap prefix)))
      (delete (last key) submap)
      (when (= (length submap) 1)
        (remove-key keymap prefix)))))

Thanks to tarsius, the above code works.

I used remove-key to remove troublesome cua keys which, when set to nil using define-key caused C-x to behave unpredictably, if any reaction at all.

(remove-key cua--prefix-repeat-keymap (kbd "C-x"))
(remove-key cua-global-keymap (kbd "<C-return>"))

The C-x declaration is successfully removed. Unfortunately, cua is somehow still affecting C-return is still a problem, but remove-key did remove the declaration as promised. Some interlinks can get complex. Using remove-key get rid of the declaration actually simplifies the situation.

4
  • This seems to be an (elaborate) comment on another answer - like a QA report on that answer. It doesn't seem to answer the question.
    – Drew
    Commented May 10, 2021 at 23:10
  • I do not have enough reputation to comment, or I would have in a much shorter manner and it would have been better.
    – Paul
    Commented May 11, 2021 at 1:03
  • Paul: Maybe just clarify that kmu-parse-key-description is available in the package, but this function you have supplied works in a stand-alone fashion. Commented May 19, 2021 at 19:55
  • I understand that although you would have preferred to comment, you wrote an actual answer instead - so I'm removing the "not-an-answer" flag and leaving the answer as it is. If you wish to improve on this, you should make more clear how the post answers the question, but for now I'll take as given that this is an attempt to answer.
    – Aaron Hall
    Commented May 20, 2021 at 17:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.