notes from /dev/null

by Charles Choi 최민수


Rethinking Minibuffer Movement

18 Dec 2024  Charles Choi

For your consideration, a possible quality of life improvement in using Emacs. Typically the next-to-the-smallest unit of point movement is by word. This is reinforced by binding M-f and M-b to forward and backwards word movement respectively.

When editing prose, this is sensible. For editing code and commands, I would argue this is less so. This is because the unit of text I most want to move by is via symbol. Common examples of symbols include variable/function/class names, command line options & arguments, and Makefile targets. Symbols frequently embed non-alphabetic characters (e.g. ‘foo-bar’, ‘bar_foo’, ‘foo/bar’) which are treated as word-separators by the commands forward-word and backward-word.

Movement by symbol is what I want whenever I’m editing in the minibuffer. Muscle memory wants me to type M-f or M-b, which does the “wrong” thing here. But there are movement commands for a unit of text that handles symbols gracefully: that unit of text is called a balanced expression (aka sexp). By default, moving by balanced expression is bound to C-M-f for forwards movement, C-M-b for backwards.

Personally, I’m loathe to use keybindings involving more than two keys, so I’ve taken this tack: Swap (M-f, M-b) for balanced expression movement and (C-M-f, C-M-b) for word movement in modes that involve a lot of symbols.

Another detail to consider is moving the point so that it is at the start of a unit of text. Both forward-word and forward-sexp are implemented so that the point is set at the end of a text unit. In many cases this adds editing friction for me because what I really want is for the point to be at the start of a text unit. To allow for this, I’ve implemented a function for moving forward a balanced expression that places the point at the start of the next sexp (cc/next-sexp).

Code showing the implementation of cc/next-sexp and configuring the minibuffer to swap the bindings for word and sexp movement is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(keymap-set minibuffer-mode-map "M-b" #'backward-sexp)
(keymap-set minibuffer-mode-map "M-f" #'cc/next-sexp)
(keymap-set minibuffer-mode-map "C-M-b" #'backward-word)
(keymap-set minibuffer-mode-map "C-M-f" #'forward-word)

(defun cc/--next-sexp-raw ()
  "Raw implementation to move point to the beginning of the next sexp.

This function has no error checking."
  (forward-sexp 2)
  (backward-sexp))

(defun cc/next-sexp ()
  "Move point to beginning of the next balanced expression (sexp)."
  (interactive)
  (condition-case nil
      (cc/--next-sexp-raw)
    (error (condition-case nil
               (forward-sexp)
             (error
              (message
               "Unable to move point to next balanced expression (sexp)."))))))

The same swap can be applied when calling eval-expression (M-:). The keymap to configure here is minibuffer-local-shell-command-map.

1
2
3
4
(keymap-set minibuffer-local-shell-command-map "M-b" #'backward-sexp)
(keymap-set minibuffer-local-shell-command-map "M-f" #'cc/next-sexp)
(keymap-set minibuffer-local-shell-command-map "C-M-b" #'backward-word)
(keymap-set minibuffer-local-shell-command-map "C-M-f" #'forward-word)

Why stop there? Let’s change it for Elisp mode:

1
2
3
4
(keymap-set emacs-lisp-mode-map "M-b" #'backward-sexp)
(keymap-set emacs-lisp-mode-map "M-f" #'cc/next-sexp)
(keymap-set emacs-lisp-mode-map "C-M-b" #'backward-word)
(keymap-set emacs-lisp-mode-map "C-M-f" #'forward-word)

This idea can be extended to other modes such as Eshell.

Closing Thoughts

I’ve been living with this setup for a month now and anecdotally I’ve found this to feel “right” enough to merit making a post. For those readers who do not consider this post’s suggestion heretical, I’d encourage to give these changes a try. Perhaps you’ll find yourself pleasantly surprised.

emacs

 

AboutMastodonInstagramGitHub

Feeds & TagsGet Captee for macOS

Powered by Pelican