Skip to content

Latest commit

 

History

History
519 lines (447 loc) · 23.7 KB

ha-evil.org

File metadata and controls

519 lines (447 loc) · 23.7 KB

On the Subject of Being Evil

A literate programming file for configuring Evil mode in Emacs.

Introduction

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 or a jumps into a state of total Emacs, with the exception of Escape going back to Evil. This means, that typing C-p goes up a line, and doesn’t auto-complete.
  • I don’t use : and instead use M-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.

screenshots/ha-leader.png

Some advice that I followed:

TODO: Rebind the z keys

Evil-Specific Keybindings

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 is a, it gets rid of the surrounding whitespace as well. For instance, I mainly use das and cis.
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 instance
    xa”
    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 ], see x
x
within a brace, paren, etc., with the my extensions below, see b and f 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 the a 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))

Evil Text Object Line

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?

Text Objects based on Indentation

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.

Arguments as Text Objects

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

Better Parenthesis with Text Object

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)

Text Object for Functions

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.

Evil Extensions

Evil Exchange

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, or o 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

Evil Lion

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))))

Evil Commentary

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)))

Evil Collection

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)

Evil Owl

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))

Evil Surround

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 ].

Evil Jump, er Better Jump

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)))

Technical Artifacts

Let’s provide a name so we can require this file: