How to (re)bind keys

Every Emacs user will want to (re)bind keys at some point, but keybindings can be a complicated affair in Emacs, what with all the keymaps and hierarchy of precedence. This guide walks you through the concepts and how to bind keys in a number of scenarios.

Concepts

Key sequences

Emacs represents a sequence of key presses in Elisp a number of ways:

  • As a character code:
    Key Emacs Lisp
    Tab ?\t
    x ?x
    Ctrlc ?\C-c
  • As a vector of character codes and/or events:
    Key Emacs Lisp
    Tab [?\t]
    x [?x]
    Ctrlc, c [?\C-c ?c]
    Tab, Left Arrow, Left Mouse [tab left down-mouse-1]
    Shift+Tab [backtab]
  • Or as a key string:
    Key Emacs Lisp
    Tab (kbd "TAB") or (kbd "<tab>")
    Ctrlc, c (kbd "C-c c")
    SPC, x, y, z (kbd "SPC x y z")

Emacs can’t understand key strings directly, so they must be converted to an internal representation using the kbd function:

(global-set-key (kbd "TAB") #'some-command)
(global-set-key (kbd "C-c C-c") #'another-command)

:warning: Doom’s map! macro implicitly wraps its keys in kbd so you don’t have to.

:information_source: Some keys have multiple representations, like "TAB" and "<tab>".

  • (kbd "TAB") evaluates to "\t"
  • (kbd "<tab>") evaluates to [tab].

They are not interchangeable. e.g. Terminal emacs won’t understand [tab] (but GUI Emacs will). If you are binding a key to tab it’s best to bind to both for maximum coverage.

Prefixes

A prefix is a key that begins a key sequence. For example, the key sequence C-xC-kb is comprised of three keypresses. Both C-x and C-xC-k are prefixes.

Emacs will wait indefinitely for you to complete a key sequence. This may be unusual to vim users, where the editor will time out after a moment. To “abort” a key sequence, press C-g.

:information_source: C-g isn’t actually an abort procedure in the formal sense. We’re exploiting the fact that Emacs will throw a harmless error when you type a key sequence that has no command bound to it. The error stops Emacs from waiting for your next key.

By unwritten convention Emacs, Doom, and third party packages avoid binding to C-g anywhere, so it’s safe to exploit for this purpose.

Keymaps

Emacs reads keymaps to determine what to do when you type in a key sequence. A keymap is a mapping of key sequences to commands (and each key=>command mapping is a keybind). At any time Emacs has a hierarchy of active keymaps, all vying for precedence.

Keymaps with higher precedence will override keymaps with lower precedence. i.e. If you press a key, Emacs will travel down the list of active keymaps from highest to lowest precedence until it finds a matching keybind.

Below I describe the most common keymaps you will encounter, but this is not an exhaustive list.

:information_source: Look up keymaps with M-x helpful-variable (SPChv for evil users, C-hv for vanilla users).

  • global-map is the global keymap and is always active, but has the lowest precedence.

    :warning: Unless you are targeting buffers where evil is disabled — or in emacs state — it is rarely wise for evil users to bind keys to this keymap, because evil’s global keymaps have higher precedence (see “Evil States” in the next section).

    (map! "C-x C-f" #'counsel-find-file
          "C-x C-c" #'save-buffers-kill-terminal)
    
    (global-set-key (kbd "C-x C-f") #'counsel-find-file)
    (global-set-key (kbd "C-x C-c") #'save-buffers-kill-terminal)
    
    (define-key global-map (kbd "C-x C-f") #'counsel-find-file)
    (define-key global-map (kbd "C-x C-c") #'save-buffers-kill-terminal)
    
  • Major modes have their own keymap {MAJOR-MODE}-map, and should be used for keybinds that you want restricted to a specific major mode (i.e. mode-local keybinds).

    Major mode keymaps have higher precedence than global-map, but lower than minor mode keymaps, and are only active when the major mode is.

    (map! :after python
          :map python-mode-map
          :prefix "C-x C-p"
          "f" #'python-pytest-file
          "r" #'python-pytest-repeat)
    
    (with-eval-after-load 'python
      (define-key python-mode-map (kbd "C-x C-p f") #'python-pytest-file)
      (define-key python-mode-map (kbd "C-x C-p r") #'python-pytest-repeat))
    
  • Minor modes have their own keymaps as well {MINOR-MODE}-map, and some packages may define more than one (for example, helm has helm-map, helm-grep-map, helm-bookmark-map, etc). These are only active when their associated minor mode is active.

    Minor mode keymaps have higher precedence than major mode keymaps. Their priority relative to other minor modes is sorted by activation order, with recent minor modes having higher precedence.

  • The general.el package provides general-override-mode-map, which is a special keymap that tries to have precedence over most other keymaps (including minor modes’). Doom only binds two things to this keymap:

    1. Its leader keys (not its localleader keys)
    2. M-x (so it doesn’t get overwritten by a careless package/minor mode)

    Use this keymap if you want to bind a key you want nothing else to override. e.g. another leader key:

    ;; Binds a second leader key to C-x C-x (original key is still
    ;; on C-c for vanilla users and SPC for evil users)
    (map! :map 'override "C-x C-x" #'doom/leader)
    
    ;; Binds a second leader key to C-x C-x (original key is still
    ;; on C-c for vanilla users and SPC for evil users)
    (general-def 'normal 'override "C-x C-x" #'doom/leader)
    
    ;; Binds a second leader key to C-x C-x (original key is still
    ;; on C-c for vanilla users and SPC for evil users)
    (define-key general-override-mode-map (kbd "C-x C-x") #'doom/leader)
    

Evil states

Evil users have yet another layer of keymaps to contend with: for states.

:warning: What evil calls “states”, vim users know as “modes”. e.g. the normal/visual/insert modes are referred to as normal/visual/insert states in evil’s internals.

Evil’s state keymaps come in three flavors:

  • The global keymaps that evil defines for each of its states: evil-normal-state-map, evil-visual-state-map, evil-insert-state-map, etc. The bindings in these maps are visible in all buffers that are in the corresponding state.
  • The buffer-local state keymaps: evil-normal-state-local-map, evil-visual-state-local-map, etc. Keys bound here are restricted to the buffer they belong to (i.e. they are buffer-local keybinds), and will be available when that buffer is in the corresponding state.
  • The special nested ones evil creates in existing keymaps for each of its states (which it calls auxiliary keymaps). For instance, (evil-define-key* 'normal python-mode-map "x" #'do-something) creates an auxiliary keymap inside python-mode-map that is only active when both python-mode is active and you are in evil’s normal mode.

Precedence with evil keymaps is more complicated. In an attempt to over-simplify, these are listed in order of lowest to highest precedence:

  • global-map
  • python-mode-map
  • evil-normal-state-map
  • evil-normal-state-local-map
  • normal auxiliary map on python-mode-map

Which-key

Doom installs the which-key plugin, which presents these popups while Emacs is waiting for you to complete a key sequence:

image

How to define your own labels or change these are described in How to bind > Which-key labels below.

How to bind

Global keys

(map! "C-x C-r" #'git-gutter:revert-hunk
      "C-x C-b" #'ibuffer
      "C-x C-l" #'+lookup/file)
;; or
(map! :prefix "C-x"
      "C-r" #'git-gutter:revert-hunk
      "C-b" #'ibuffer
      "C-l" #'+lookup/file)
(general-define-key :prefix "C-x"
                    "C-r" #'git-gutter:revert-hunk
                    "C-b" #'ibuffer
                    "C-l" #'+lookup/file)
(global-set-key (kbd "C-x C-r") #'git-gutter:revert-hunk)
(global-set-key (kbd "C-x C-b") #'ibuffer)
(global-set-key (kbd "C-x C-l") #'+lookup/file)
;; or
(let ((map global-map))
  (define-key map (kbd "C-x C-r") #'git-gutter:revert-hunk)
  (define-key map (kbd "C-x C-b") #'ibuffer)
  (define-key map (kbd "C-x C-l") #'+lookup/file))

Mode or buffer-local keys

It is important to bind your keys after the keymap is defined, for two reasons:

  • You will get void-variable errors if you try to bind keys to a keymap that isn’t defined.
  • Doom or other packages may bind their own default keys to that keymap – running yours after ensures yours override the defaults and not the other way around.

The key to achieving this is one of: the after! macro, map!'s :after keyword, or with-eval-after-load:

(map! :after python
      :map python-mode-map
      "C-x C-r" #'python-shell-send-region)
;; or
(after! python
  (map! :map python-mode-map "C-x C-r" #'python-shell-send-region))
(general-define-key :package 'python
                    :prefix "C-x"
                    "C-r" #'python-shell-send-region)
(with-eval-after-load 'python
  (define-key python-mode-map (kbd "C-x C-r") #'python-shell-send-region))

Evil state-local keys

Which-key labels

Defining a prefix

Overriding deferred keys

Defining new evil text objects

How to unbind keys

Common pitfalls

Avoid :prefix-map

Binding to SPC instead of :leader

Key sequence ... starts with non-prefix key ... errors

Special cases

Binding keys to key sequences

  • general-simulate-key, vectors, or string key sequences

Binding keys to conditional dispatchers with cmds!

Overriding org-mode + evil-org-mode

Overriding evil-collection

Overriding a package’s default keybinds

3 Likes