A literate programming file for configuring Evil mode in Emacs.
As a grizzled veteran of the Emacs-VI Wars, I’ve decided to take advantage of both by using VI keybindings on top of Emacs. However, after thirty years of Emacs, my interface follows different goals:
- Most buffers begin in Evil’s normal state, e.g. normal mode for VIers.
- Pressing
i
ora
jumps into a state of total Emacs, with the exception ofEscape
going back to Evil. This means, that typingC-p
goes up a line, and doesn’t auto-complete. - I don’t use
:
and instead useM-x
or better yet,SPC SPC
(typing the space key twice) from General project. - The
Space
doesn’t advance a letter, but instead displays a tree of highly-customized functions, displayable at the bottom of my screen, e.g.
Some advice that I followed:
- Evil Guide
- A Vim-like Emacs Configuration from Nathan Typanski
- Evil insert state is really Emacs? Real answer to that is to set evil-disable-insert-state-bindings
TODO: Rebind the z
keys
I split the configuration of Evil mode into sections. First, global settings:
(use-package evil
:init
(setq evil-undo-system 'undo-fu
evil-auto-indent t
evil-respect-visual-line-mode t
evil-want-fine-undo t ; Be more like Emacs
evil-disable-insert-state-bindings t
evil-want-keybinding nil ; work with evil-collection
evil-want-integration t
evil-want-C-u-scroll nil
evil-want-C-i-jump nil
evil-escape-key-sequence "jk"
evil-escape-unordered-key-sequence t))
The Escape key act like C-g
and always go back to normal mode?
(use-package evil
:config
;; (global-set-key (kbd "<escape>") 'keyboard-escape-quit)
;; Let's connect my major-mode-hydra to a global keybinding:
(evil-define-key 'normal 'global "," 'major-mode-hydra)
(evil-mode))
Even with the Evil Collection, some modes should start in the Emacs state:
(use-package evil
:config
(dolist (mode '(custom-mode
dired-mode
eshell-mode
git-rebase-mode
erc-mode
circe-server-mode
circe-chat-mode
circe-query-mode
vterm-mode))
(add-to-list 'evil-emacs-state-modes mode)))
I’m not a long term VI user, and I generally like easy keys, e.g. w
, have larger jumps, and harder keys, e.g. W
(shifted), have smaller, fine-grained jumps. So I am switching these around:
(use-package evil
:config (require 'evil-commands)
(evil-define-key
'(normal visual motion operator) 'global
"w" 'evil-forward-WORD-begin
"W" 'evil-forward-word-begin
"e" 'evil-forward-WORD-end
"E" 'evil-forward-word-end)
The b
key bindings seems to need their own call, and I’m not sure why I can’t include it in the w
and e
bindings.
(evil-define-key
'(normal visual motion operator) 'global
"b" 'evil-backward-WORD-begin)
(evil-define-key
'(normal visual motion operator) 'global
"B" 'evil-backward-word-begin)
In other words, with the above settings in place, w
and e
should jump from front to back of the entire line, but W
and E
should stop as subword:
word-subword-subword
word_subword_subword
Note I don’t bind evil-backward-word-end
with a single key, but bind it to g e
below.
While an absolute heresy to most VI users, I’m Evil and use M-x
and SPC
instead of :
for running commands. I bind the :
as a reverse of the ;
which continues the search from f
and t
:
(evil-define-key
'(normal visual motion operator) 'global
":" 'evil-repeat-find-char-reverse))
This clever hack from Manuel Uberti got me finding these useful bindings:
g ;
- goto-last-change
g ,
- goto-last-change-reverse
Keybindings I would like to use more:
*
- jumps to the next instance of the word under point
#
- jumps to the previous instance of the word under point
While I’m pretty good with the VIM keybindings, I would like to play around with the text objects and how it compares to others (including the surround).
diw
- deletes a word, but can be anywhere in it, while
de
deletes to the end of the word. daw
- deletes a word, plus the surrounding space, but not punctuation.
xis
- changes a sentence, and if
i
isa
, it gets rid of the surrounding whitespace as well. For instance, I mainly usedas
andcis
. xip
- changes a paragraph.
xio
- changes a symbol, which can change for each mode, but works with
snake_case
and other larger-than-word variables.
- Surrounding punctuation, like quotes, parenthesis, brackets, etc. also work, so
ci)
changes all the parameters to a function call, for instancexa”
- a double quoted string
xi”
- inner double quoted string
xa'
- a single quoted string
xi'
- inner single quoted string
xa`
- a back quoted string
xi`
- inner back quoted string
NOTE: The x
in the above examples are operations, e.g. d
for delete, v
for select, y
for copy and c
for change.
What text objects are known?
w
- word
s
- sentence
p
- paragraph
l
- lines, with the Text Object Line package, configured below.
o
- symbol, like a variable, but also words, so
vio
is an easy sequence for selecting a word. ’
- a string, surround by quotes, also
`
for backticks )
- parenthesis, also
}
and]
, seex
x
- within a brace, paren, etc., with the my extensions below, see
b
andf
offer similar functionality. d
/f
- a defun, or code block, see Tree-Sitter approach defined here, or the old Emacs approach defined below.
i
- indention area, for YAML and Python, with the evil-indent-plus package, configured below.
t
- an HTML tag
c
- for comments
u
- for URLs, really? Useful much?
a
- function arguments (probably a lot like symbol,
o
), but thea
can include commas. This comes from evil-args extension (see below).
TODO: Search for a plugin, like textobj-word-column for text objects based on “columns”.
I am not a long term VI user, and don’t have much need for any of its control sequences (well, not all), so I made the following more Emacsy. I’ll admit, I like C-v
(and use that all the time), so I need to futz around with the scrolling:
(use-package evil
:config
(evil-define-key '(normal visual motion operator) 'global
(kbd "C-a") 'ha-beginning-of-line
(kbd "C-e") 'move-end-of-line
;; Since C-y scrolls the window down, Shifted Y goes up:
(kbd "C-y") 'evil-scroll-line-down
(kbd "C-b") 'evil-scroll-line-up
(kbd "C-S-y") 'evil-scroll-line-up
(kbd "C-d") 'scroll-down-command
(kbd "C-S-d") 'scroll-other-window-down
(kbd "C-f") 'scroll-up-command
(kbd "C-S-f") 'scroll-other-window
(kbd "C-o") 'open-line ; matches evil's o
(kbd "C-p") 'previous-line
(kbd "C-n") 'next-line
;; I have better window control:
(kbd "C-w") 'sp-kill-region))
Delete a line, d d
is in basic VI. Since some commands use text objects, and the basic text object doesn’t include lines, the evil-textobj-line project adds that:
(use-package evil-textobj-line)
Now v i l
and v a l
works as you’d expect, but does this improve on S-v
?
The evil-indent-plus project creates text objects based on the indentation level, similar to how the b
works with “blocks” of code.
(use-package evil-indent-plus)
This can be handy for Python, YAML, and lists in org files. Note that i
works for the current indent, but k
includes one line above and j
includes one line above and below.
The evil-args projects creates text objects for symbols, but with trailing ,
or other syntax.
(use-package evil-args
:config
;; bind evil-args text objects
(define-key evil-inner-text-objects-map "a" 'evil-inner-arg)
(define-key evil-outer-text-objects-map "a" 'evil-outer-arg)
;; bind evil-forward/backward-args
(define-key evil-normal-state-map "L" 'evil-forward-arg)
(define-key evil-normal-state-map "H" 'evil-backward-arg)
(define-key evil-motion-state-map "L" 'evil-forward-arg)
(define-key evil-motion-state-map "H" 'evil-backward-arg)
;; bind evil-jump-out-args
(define-key evil-normal-state-map "K" 'evil-jump-out-args))
For a function, like this Python example, with the cursor on b
:
def foobar(a, b, c):
return a + b + c
Typing d a a
will delete the argument leaving:
def foobar(a, c):
return a + b + c
I took the following clever idea and code from this essay from Chen Bin for creating a xix
to grab code within any grouping characters, like parens, braces and brackets. For instance, dix
cuts the content inside brackets, etc. First, we need a function to do the work (I changed the original from my-
to ha-
so that it is easier for me to distinguish functions from my configuration):
(defun ha-evil-paren-range (count beg end type inclusive)
"Get minimum range of paren text object.
COUNT, BEG, END, TYPE follow Evil interface, passed to
the `evil-select-paren' function.
If INCLUSIVE is t, the text object is inclusive."
(let* ((open-rx (rx (any "(" "[" "{" "<")))
(close-rx (rx (any ")" "]" "}" ">")))
(range (condition-case nil
(evil-select-paren
open-rx close-rx
beg end type count inclusive)
(error nil)))
found-range)
(when range
(cond
(found-range
(when (< (- (nth 1 range) (nth 0 range))
(- (nth 1 found-range) (nth 0 found-range)))
(setf (nth 0 found-range) (nth 0 range))
(setf (nth 1 found-range) (nth 1 range))))
(t
(setq found-range range))))
found-range))
Extend the text object to call this function for both inner and outer:
(evil-define-text-object ha-evil-a-paren (count &optional beg end type)
"Select a paren."
:extend-selection t
(ha-evil-paren-range count beg end type t))
(evil-define-text-object ha-evil-inner-paren (count &optional beg end type)
"Select 'inner' paren."
:extend-selection nil
(ha-evil-paren-range count beg end type nil))
And the keybindings:
(define-key evil-inner-text-objects-map "x" #'ha-evil-inner-paren)
(define-key evil-outer-text-objects-map "x" #'ha-evil-a-paren)
While Emacs has the ability to recognize functions, the Evil text object does not. But text objects have both an inner and outer form, and what does that mean for a function? The inner will be the function itself and the outer (like words) would be the surrounding non-function stuff … in other words, the distance between the next functions.
(defun ha-evil-defun-range (count beg end type inclusive)
"Get minimum range of `defun` as a text object.
COUNT, is the number of _following_ defuns to count. BEG, END,
TYPE are not used. If INCLUSIVE is t, the text object is
inclusive acquiring the areas between the surrounding defuns."
(let ((start (save-excursion
(beginning-of-defun)
(point)))
(end (save-excursion
(end-of-defun count)
(point))))
(when inclusive
;; Let's see if we can grab more text ...
(save-excursion
;; Don't bother if we are at the start of buffer:
(when (> start (point-min))
(goto-char start)
;; go to the end of the previous function:
(beginning-of-defun)
(end-of-defun count)
;; if we found some more text to grab, reset start:
(if (< (point) start)
(setq start (point))))
;; Same approach with the end:
(when (< end (point-max))
(goto-char end)
(end-of-defun)
(beginning-of-defun)
(if (> (point) end)
(setq end (point))))))
(list start end)))
Extend the text object to call this function for both inner and outer:
(evil-define-text-object ha-evil-a-defun (count &optional beg end type)
"Select a defun and surrounding non-defun content."
:extend-selection t
(ha-evil-defun-range count beg end type t))
(evil-define-text-object ha-evil-inner-defun (count &optional beg end type)
"Select 'inner' (actual) defun."
:extend-selection nil
(ha-evil-defun-range count beg end type nil))
And the keybindings:
(define-key evil-inner-text-objects-map "d" #'ha-evil-inner-defun)
(define-key evil-outer-text-objects-map "d" #'ha-evil-a-defun)
Why not use f
? I’m reserving the f
for a tree-sitter version that is not always available for all modes… yet.
I often use the Emacs commands, M-t
and whatnot to exchange words and whatnot, but this requires a drop out of normal state mode. The evil-exchange project attempts to do something similar, but in a VI-way, and the objects do not need to be adjacent.
(use-package evil-exchange
:init
(setq evil-exchange-key (kbd "gx")
evil-exchange-cancel-key (kbd "gX"))
:general (:states 'normal
"g x" '("exchange" . 'evil-exchange)
"g X" '("cancel exchange" . 'evil-exchange-cancel)
;; What about a "normal mode" binding to regular emacs transpose?
"z w" '("transpose words" . transpose-words)
"z x" '("transpose sexps" . transpose-sexps)
"z k" '("transpose lines" . transpose-lines))
:config (evil-exchange-install))
Let’s explain how this works as the documentation assumes some previous knowledge. If you had a sentence:
The ball was blue and the boy was red.
Move the point to the word, red, and type g x i w
(anywhere since we are using the inner text object). Next, jump to the word blue, and type the sequence, g x i w
again, and you have:
The ball was blue and the boy was red.
The idea is that you can exchange anything. The g x
marks something (like what we would normally do in visual mode), and then by marking something else with a g x
sequence, it swaps them.
Notice that you can swap:
gx i w
- words,
W
words with dashes, oro
for programming symbols (like variables) gx i s
- sentences
gx i p
- paragraphs
gx i x
- programming s-expressions between parens, braces, etc.
gx i l
- lines, with the line-based text object project installed
The evil-lion package is a wrapper around Emacs’ align function. Just a little easier to use. Primary sequence is g a i p =
to align along all the equal characters in the paragraph (block), or g a i b RET
to use a built in rule to align (see below), or g a i b /
to specify a regular expression, similar to align-regexp.
(use-package evil-lion
:after evil
:general
(:states '(normal visual)
"g a" '("lion ←" . evil-lion-left)
"g A" '("lion →" . evil-lion-right)))
Lion sounds like align … get it?
Where I like to align, is on variable assignments, e.g.
(let ((foobar "Something something")
(a 42)
(very-long-var "odd string"))
;;
)
If you press RETURN
for the character to align, evil-lion
package simply calls the built-in align function. This function chooses a regular expression based on a list of rules, and aligning Lisp variables requires a complicated regular expression. Extend align-rules-list:
(use-package align
:straight (:type built-in)
:config
(add-to-list 'align-rules-list
`("lisp-assignments"
(regexp . ,(rx (group (one-or-more space))
(or
(seq "\"" (zero-or-more any) "\"")
(one-or-more (not space)))
(one-or-more ")") (zero-or-more space) eol))
(group . 1)
(modes . align-lisp-modes))))
The evil-commentary is a VI-like way of commenting text. Yeah, I typically type M-;
to call Emacs’ originally functionality, but in this case, g c c
comments out a line(s), and g c
comments text objects and whatnot. For instance, g c $
comments to the end of the line.
(use-package evil-commentary
:config (evil-commentary-mode)
:general
(:states '(normal visual motion operator)
"g c" '("comments" . evil-commentary)
"g y" '("yank comment" . evil-commentary-yank)))
Dropping into Emacs state is better than pure Evil state for applications, however, the evil-collection package creates a hybrid between the two, that I like.
(use-package evil-collection
:after evil
:config
(evil-collection-init))
Do I want to specify the list of modes to change for evil-collection-init
, e.g.
'(eww magit dired notmuch term wdired)
Not sure what is in a register? Have it show you when you hit ”
or @
with evil-owl:
(use-package posframe)
(use-package evil-owl
:after posframe
:config
(setq evil-owl-display-method 'posframe
evil-owl-extra-posframe-args
'(:width 50 :height 20 :background-color "#444")
evil-owl-max-string-length 50)
(evil-owl-mode))
I like both evil-surround and Henrik’s evil-snipe, but they both start with s
, and conflict, and getting them to work together means I have to remember when does s
call sniper and when it calls surround. As an original Emacs person, I am not bound by that key history, but I do need them consistent, so I’m choosing the s
to be surround.
(use-package evil-surround
:config
(defun evil-surround-elisp ()
(push '(?\` . ("`" . "'")) evil-surround-pairs-alist))
(defun evil-surround-org ()
(push '(?\" . ("“" . "”")) evil-surround-pairs-alist)
(push '(?\' . ("‘" . "’")) evil-surround-pairs-alist)
(push '(?b . ("*" . "*")) evil-surround-pairs-alist)
(push '(?* . ("*" . "*")) evil-surround-pairs-alist)
(push '(?i . ("/" . "/")) evil-surround-pairs-alist)
(push '(?/ . ("/" . "/")) evil-surround-pairs-alist)
(push '(?= . ("=" . "=")) evil-surround-pairs-alist)
(push '(?~ . ("~" . "~")) evil-surround-pairs-alist))
(global-evil-surround-mode 1)
:hook
(org-mode . evil-surround-org)
(emacs-lisp-mode . evil-surround-elisp))
Notes:
cs'"
- to convert surrounding single quote string to double quotes.
ds"
- to delete the surrounding double quotes.
yse"
- puts single quotes around the next word.
ysiw'
- puts single quotes around the word, no matter the points position.
yS$<p>
- surrouds the line with HTML
<p>
tag (with extra carriage returns). ysiw'
- puts single quotes around the word, no matter the points position.
(
- puts spaces inside the surrounding parens, but
)
doesn’t. Same with[
and]
.
The better-jumper project replaces the evil-jumper project, essentially allowing you jump back to various movements. While I already use g ;
to jump to the last change, this jumps to the jumps … kinda. I’m having a difficult time determining what jumps are remembered.
(use-package better-jumper
:config
(better-jumper-mode +1)
(with-eval-after-load 'evil-maps
(define-key evil-motion-state-map (kbd "M-[") 'better-jumper-jump-backward)
(define-key evil-motion-state-map (kbd "M-]") 'better-jumper-jump-forward)))
Let’s provide
a name so we can require
this file: