Modal editing (evil)
Macros offers modal editing in two flavors, both built entirely on the editor's keymaps, both opt-in, and both fully overridable from Scheme:
- evil (this page) — Vim-style, verb-then-motion (
d wdeletes a word). Enable with(evil-mode). - helix — selection-first, motion-then-verb (
wselects a word,ddeletes it), with first-class multiple selections. Enable with(helix-mode).
Turn on one or the other, not both. Macros uses standard Emacs bindings unless you opt in — modal editing is not on by default.
evil-mode
Macros ships with evil-mode: Vim-style modal editing that mirrors GNU Emacs's evil-mode — plain Vim modal editing, no leader key.
Enable it from your init.scm (see Configuration):
(evil-mode) ; opt in to Vim-style modal editing
(evil-disable) ; turn it back off
The Rust side only decides when plain keys are commands (Normal/Visual states) versus text (Insert state); every binding is Scheme and fully overridable.
States
- Normal — keys are commands. This is where you start.
- Insert — keys insert text. Enter with i / a / o (and capitals); leave with Esc.
- Visual — character selection for operators. Enter with v.
Esc (keyboard-quit) always returns you to Normal.
Normal-state motions
| Key | Moves |
|---|---|
| h j k l | left / down / up / right |
| w b e g e (and W B E g E) | word / WORD motions |
| 0 ^ $ | start / first non-blank / end of line |
| g g / G | first / last line (5 G: line 5) |
| { } / ( ) | paragraph / sentence |
| % | matching bracket |
| f F t T + char, ; , | find on the line, repeat |
| H M L | top / middle / bottom of the window |
| ] m [ m (] f [ f), ] M [ M | next / previous function start, end (tree-sitter) |
| ] ] [ [, ] [ [ ] | next / previous class (section) start, end |
| ] d [ d | next / previous diagnostic |
Counts work everywhere: 3 w, 5 j, 2 ] m.
Entering insert state
| Key | Action |
|---|---|
| i / I | insert before cursor / at first non-blank |
| a / A | append after cursor / at end of line |
| o / O | open a line below / above |
In Insert state, C-r + a register name inserts that register.
Operators
Operators combine with any motion or text object, with a count on either side — Vim's {count}{operator}{count}{motion}: d 2 w, 2 d 3 w, c i ", y a p, g U i w, > i p, d / foo RET, d ' a. Doubling the operator acts on lines (d d, 3 > >, g U U, g c c), and v / V after the operator force the motion charwise / linewise (d v j). Motions are exclusive, inclusive, or linewise exactly as in Vim.
| Operator | Does |
|---|---|
| d c y | delete / change / yank |
| > < = | shift right / left, reindent |
| g u g U g ~ g ? | lowercase / uppercase / toggle case / rot13 |
| g q g w | format to *fill-column* (gw keeps the cursor) |
| g c | toggle line comments |
Text objects: i w a w, quotes (i " i ' i `), brackets (i ( / i b, i { / i B, i [, i <), paragraphs (i p), and — from the syntax tree — functions (a f i f), classes / types (a c i c), arguments / parameters (a a i a) and comments (a / i /). In Visual state the same keys select the object, and operator keys act on the selection.
Single-key edits: x s S D C Y r ~ J g J p P, u / C-r undo / redo. C-a increments the number at or after the cursor (decimal, hex, binary; with a count) and g - decrements; in Visual, C-a / g C-a / g C-x work per line. Vim's C-x decrement is off by default because C-x is the Emacs prefix — enable it with (evil-enable-ctrl-x-decrement).
Repeat with .
. repeats the last change — an operator with its motion, an Insert session (c w … Esc, o …, A …), x, r, p, J, > >, and so on. A count replaces the original one (3 .). Each change, including its Insert session, undoes as one step.
Registers
" + a name picks the register for the next yank, delete, or put: "a–"z ("A–"Z append), "0 (last yank), "1–"9 (deleted lines), "- (small deletes), "_ (black hole), "+ / "* (system clipboard), and the read-only ". (last insert), ": (last command line), "/ (last search). The unnamed register is the kill ring, kept in sync with the system clipboard. :registers lists them.
Macros
q a records into register a (q A appends), q stops; @ a replays (3 @ a), @ @ repeats the last one, @ : the last ex command.
Marks and jumps
m a sets a mark (m A a file mark), ' a jumps to its line, a</kbd> to the exact spot. Marks move with edits. Automatic marks: <kbd>' '</kbd> (before the last jump),'[ '](last change/yank),'< '>(last Visual area — <kbd>g v</kbd> reselects it),'.and'^. :marks` lists them.
Jumps (G, /, n, %, {, marks, g d, …) are recorded: C-o goes back, C-i (or Tab) forward; :jumps lists them.
Windows
C-w then: h j k l move between windows, w / W cycle, p previous; s / v split, n new buffer, c / q close, o only; = balance, + - < > resize, _ | maximize; H J K L move the window to the far side, x exchange, r rotate. Emacs users get windmove-left/-right/-up/-down and balance-windows (C-x +).
Folds
z a toggle, z c / z o close / open, z M / z R fold / open everything, z j / z k next / previous fold.
Search
| Key | Action |
|---|---|
| / / ? | search forward / backward |
| n / N | next / previous match |
| * / # | search the word under the cursor |
Ex commands
Press : to enter an Ex command (from Visual it starts with '<,'>). Ranges: %, ., $, line numbers, 'a marks, /pat/ and ?pat?, with +n / -n offsets and , or ; between.
:%s/old/new/g substitute (flags: g c i I n e &, and a count)
:'<,'>s/x/y/c …asking y/n/a/q/l for each match
:&& & g& repeat the last substitute
:g/pat/d :v/pat/d run a command on (non-)matching lines
:g/TODO/normal A! …including Normal-state keys
:%norm A; run keys on every line of a range
:5 :$ :.+3 go to a line
:d :y :m0 :t. :co$ :j :> :< :sort[!] [inu] :put
:noh :registers :marks :jumps :new :vnew :wincmd h
Scrolling
| Key | Action |
|---|---|
| C-d / C-u | half-page down / up |
No leader key by default
Like vanilla evil-mode in Emacs, Macros' evil has no Space leader bindings. A leader menu is a Doom/Spacemacs convention, not part of evil itself, so it's left to you. Adding one is a few lines in your init.scm:
;; Roll your own Spacemacs-style leader. Commands are 'name symbols (a bare
;; command identity or a string name work too).
(evil-set-leader "space") ; the default; set BEFORE binding
(evil-leader-set "space" 'helm-execute-command) ; SPC SPC
(evil-leader-set "f f" 'helm-find-files)
(evil-leader-set "b b" 'helm-buffers)
(evil-leader-set "n o" 'delete-other-windows)
Leader chords bound with evil-leader-set work everywhere, not only in file buffers:
- Special buffers (magit, dired, package dashboards like
k8s-dashboard) — SPC reaches your leader even though these buffers drive their own keymaps. - Terminals and Insert state, where SPC has to type a space — use the alt leader instead, M-SPC by default (Doom's
doom-leader-alt-key): M-SPC n o in a terminal runs the same command as SPC n o in a file. Change it with(evil-set-leader-alt "ctrl-space"), or disable it with(evil-set-leader-alt ""), before your bindings.
Per-mode leader bindings shadow the global ones in that mode, including special-buffer modes:
(evil-leader-set-for-modes '("rust" "go")
(list (cons "g g" 'lsp-find-definition)))
(evil-leader-set-for-modes '("k8s-mode")
(list (cons "k r" 'k8s-refresh)))
Customizing
evil bindings live in the "evil-normal", "evil-insert", "evil-visual" (and -line / -block) maps, and motions / text objects for operators in "evil-operator". Rebind exactly like any other key:
(define-key "evil-normal" "shift-q" 'kill-buffer)
Because it's all Scheme, you can add operators, motions, and text objects of your own:
;; An operator: bind a key to (evil-operator "name") and define what it does
;; with the range [start, end) of kind "char" or "line".
(evil-define-operator "sort-lines"
(lambda (kind start end) (evil-ex (string-append (number->string (+ 1 (line-of-pos start))) ","
(number->string (+ 1 (line-of-pos (- end 1)))) "sort"))))
(define-key "evil-normal" "g s" "evil-operator \"sort-lines\"")
;; A motion for operators: return (list type (point) target).
(evil-define-motion "next-blank-line"
(lambda () (list "line" (point) (paragraph-forward-pos (point)))))
(define-key "evil-operator" "g b" "evil-motion \"next-blank-line\"")
;; Tree-sitter node kinds for a language the defaults miss.
(evil-ts-set-kinds "kotlin:function" '("function_declaration"))
A key binding whose command contains %c reads one more key as its argument (that is how m, ", @, and f under an operator work). Commands registered with (set-jump-command! 'name) are recorded in the jump list. See Scripting with Steel.