Skip to main content
Site Icon Mysterious Pixel

Emacs, snakes and camels

Camels and snakes are the same

I'm an enthusiastic user of Emacs and Emacs Evil mode, which brings Vi/Vim key-bindings to Emacs. Something that has caused a small amount of distress of late has been the inconsistency in the way Emacs handles snake_case and camelCase - something I'm hitting a lot at the moment, as I'm frequently switching between Godot's GDScript and C# bindings.

The problem is that in evil-mode a word movement like w moves by whole words for camelCase and PascalCase, and by sub-words for snake_case. For example, if the cursor is at the beginning of a function called SomeFunc in C#, or some_func in Python/GDScript.

This is clearly the devil's work, not to mention the havoc it plays with muscle memory. What I really want is the behaviour of Vim when using the CamelCaseMotion script - that is:

  1. Default to whole-word movement for fast and consistent navigation.
  2. Provide an option for granular sub-word movement when needed.
  3. Provide an option for granular sub-word editing when needed.

This seems oddly difficult in Evil - there's a lot of information about the general inconsistency, but the existing solutions don't seem entirely satisfactory.

Hacking a solution

What's frustrating is that all the necessary functionality is built-in; all I want is:

The challenge is there's no easy way to combine them. Once forward-evil-symbol is set to replace forward-evil-word, subword-mode is ignored, and can't be used. What we need is a way to temporarily switch back to forward-evil-word in specific scenarios.

After a bit of hacking, there seems to be a relatively simple way of doing it, just by storing the original value of forward-evil-word and using scoped function overrides:

;; Turn on subword-mode everywhere
(global-subword-mode t)

;; Backup the original 'forward-evil-word' function before overriding it.
(fset 'original-forward-evil-word (symbol-function 'forward-evil-word))

;; From the Evil FAQ.
;; Defaults all word movements, including editing operations, to 
;; 'whole symbols', which is what we want by default.
(defalias #'forward-evil-word #'forward-evil-symbol)

;; Create two replacement text object functions that call through to the original.
;; But they temporarily switch back to default 'forward-evil-word',
;; which respects 'subword-mode'
(defun evil-a-little-word ()
  (interactive)
  (cl-letf (((symbol-function 'forward-evil-word) 'original-forward-evil-word))
    (evil-a-word)))

(defun evil-inner-little-word ()
  (interactive)
  (cl-letf (((symbol-function 'forward-evil-word) 'original-forward-evil-word))
    (evil-inner-word)))

;; Map these text objects to 'lw'
(define-key evil-outer-text-objects-map "lw" 'evil-a-little-word)
(define-key evil-inner-text-objects-map "lw" 'evil-inner-little-word)

;; Use the same trick for optional sub-word movements
(defun evil-forward-little-word-begin (&optional COUNT BIGWORD)
  (interactive)
  (cl-letf (((symbol-function 'forward-evil-word) 'original-forward-evil-word))
    (evil-forward-word-begin COUNT BIGWORD)))

(defun evil-backward-little-word-begin (&optional COUNT BIGWORD)
  (interactive)
  (cl-letf (((symbol-function 'forward-evil-word) 'original-forward-evil-word))
    (evil-backward-word-begin COUNT BIGWORD)))


;; Bind these motions to something convenient - Ctl+w/b works for me.
(define-key evil-normal-state-map (kbd "C-w") 'evil-forward-little-word-begin)
(define-key evil-normal-state-map (kbd "C-b") 'evil-backward-little-word-begin)

There's probably some more elegant I could do using function advising, but this is good enough for now. I've been using it for a few days, and it seems to be working reliably. Hurrah!