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.
-
Pressing
w
in C# moves you past the whole function name; in Python it moves you to the underscore in the middle.
-
Using
ciw
to change the function name; in C# it replaces the entire function name, in Python it just replacessome
.
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:
- Default to whole-word movement for fast and consistent navigation.
- Provide an option for granular sub-word movement when needed.
- 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.
- The Evil mode FAQ has a whole section dedicated to it with multiple recommendations, but without an obvious way to get all the features I want. In particular
(defalias #'forward-evil-word #'forward-evil-symbol)
provides 1 (whole word movement), but no option for 2 or 3 (optional sub-word movement and editing). - Emacs' built-in
subword-mode
defaults everything to sub-word movement, and so gives 2 and 3, but sacrifices 1. It is also ignored by theforward-evil-symbol
approach above, so there's no easy way to combine them. - Emacs' built-in
superword-mode
does the opposite tosubword-mode
, and makessnake_case
behave likecamelCase
. However it doesn't work with Evil mode. - There's the old evil-little-word package, but this doesn't seem to work with newer versions of Evil.
Hacking a solution
What's frustrating is that all the necessary functionality is built-in; all I want is:
forward-evil-symbol
by default.- Optional
subword-mode
easily accessible for granular motions and text objects editing.
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!
- ← Previous
Moving from Wordpress to Eleventy