A little Vim inside your Emacs

posted on June 2, 2026
tags:

Semi modal editing

Vanilla Emacs bindings often lack the snappiness of a modal editor like Vim, especially when navigating, having to prefix each command with a bunch of C-x, C-c, C-@ and similar monstrosities can get quite tiring (or downright harmful!). It would be nice to be able to switch to some sort of a temporary modal state, where a single key press binds to a command (just like in Vim), and to get back to good old vanilla Emacs pinky workout by simply pressing any unbound key.

Implementation

Turns out that Emacs implements natively what I’ve been describing as “temporary modal states”, they are called transient-keymaps, you can think of a transient keymap as a temporary keymap that takes precedence over all the others until an unbound key is pressed.

(defun transient-next/prev-line ()
  (interactive)
  (set-transient-map
   (let ((tmap (make-sparse-map)))
     (define-key (kbd "n") #'next-line)
     (define-key (kbd "p") #'prev-line)
     tmap)))

Calling transient-next/prev-line sets up a transient keymap in which key n is bound to next-line and key p to prev-line, pressing anything else drops you out of the transient keymap. This is close to the behavior we want but our n and p bindings are not sticky like they would be in Vim, we want to be able to press n and p infinitely many times without leaving the transient keymap.

To achieve that we use some mutual recursion magic:

(defun transient-next-line ()
(interactive)
(next-line)
(set-transient-map
 (let ((tmap (make-sparse-map)))
   (define-key (kbd "n") #'transient-next-line)
   (define-key (kbd "p") #'transient-prev-line)
   tmap)))

(defun transient-prev-line ()
  (interactive)
  (prev-line)
  (set-transient-map
   (let ((tmap (make-sparse-map)))
     (define-key (kbd "n") #'transient-next-line)
     (define-key (kbd "p") #'transient-prev-line)
     tmap)))

Command transient-next-line moves point to the next line and sets up a transient keymap, from there we can press n or p to to keep moving between lines how many times we want. To wrap everything up we bind our to commands to a common prefix:

(global-set-key (kbd "C-c n") #'transient-next-line)
(global-set-key (kbd "C-c p") #'transient-prev-line)

Now after pressing either C-c p or C-c n we can keep pressing either n or p to move point, pressing anything else kicks us out of the transient. This is exactly the behavior we are looking for, mission accomplished :D. This is pretty much how Emacs handles text resizing inside a buffer via C-x C-+ and C-x C--.

Tidying up

We can get rid of all the above boilerplate by writing a macro that defines the mutually recursive commands and binds them to a common prefix for us:

(defmacro bind-transient-keymap (prefix &rest keyalist)
  "Build a transient keymap consisting of each (\"key\" . command) pair in
KEYALIST and bind it to PREFIX."
  (cl-flet ((rename-transient (symbol)
          "Add `--transient-wrapper` to SYMBOL"
          (intern (concat (symbol-name symbol) "--transient-wrapper"))))
    ;; create a --transient-wrapper for each entry in KEYALIST
    `(progn
       ,@(mapcar (lambda (pair)
           (let ((key (car pair))
             (cmd (cdr pair)))
             `(defun ,(rename-transient cmd) ()
            (interactive)
            (call-interactively #',cmd)
            (set-transient-map
             (let ((tmap (make-sparse-keymap)))
               ,@(mapcar (lambda (p)
                       (let ((k (car p))
                         (c (cdr p)))
                     `(define-key tmap
                              (kbd ,k)
                              #',(rename-transient c))))
                     keyalist)
               tmap)))))
         keyalist)
       ;; bind each --transiet-wrapper
       ,@(mapcar (lambda (pair)
           (let ((key (car pair))
             (cmd (cdr pair)))
             `(global-set-key (kbd ,(concat prefix " " key))
                      #',(rename-transient cmd))))
         keyalist))))

Replicating the example from above using our newly defined macro is as simple as:

(bind-transient-keymap "C-C"
               ("n" . next-line)
               ("p" . prev-line))