Skip to main content
Site Icon Mysterious Pixel

Emacs, snakes and camels

Camels and snakes are the same

Jump to section titled: 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

Jump to section titled: 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!