-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Expand file tree
/
Copy pathdoom-keybinds.el
More file actions
502 lines (440 loc) · 19.5 KB
/
Copy pathdoom-keybinds.el
File metadata and controls
502 lines (440 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
;;; doom-keybinds.el --- defaults for Doom's keybinds -*- lexical-binding: t; -*-
;;; Commentary:
;;
;; A centralized keybinds system, integrated with `which-key' to preview
;; available keybindings. All built into one powerful macro: `map!'. If evil is
;; never loaded, then evil bindings set with `map!' are ignored (i.e. omitted
;; entirely for performance reasons).
;;
;;; Code:
(defvar doom-leader-key "SPC"
"The leader prefix key for Evil users.")
(defvar doom-leader-alt-key "M-SPC"
"An alternative leader prefix key, used for Insert and Emacs states, and for
non-evil users.")
(defvar doom-leader-key-states '(normal visual motion)
"which evil modes to activate the leader key for")
(defvar doom-leader-alt-key-states '(emacs insert)
"which evil modes to activate the alternative leader key for")
(defvar doom-localleader-key "SPC m"
"The localleader prefix key, for major-mode specific commands.")
(defvar doom-localleader-alt-key "M-SPC m"
"The localleader prefix key, for major-mode specific commands. Used for Insert
and Emacs states, and for non-evil users.")
(defvar doom-leader-map (make-sparse-keymap)
"An overriding keymap for <leader> keys.")
;;
;;; Global keybind settings
(cond
(doom--system-macos-p
;; mac-* variables are used by the special emacs-mac build of Emacs by
;; Yamamoto Mitsuharu, while other builds use ns-*.
(setq mac-command-modifier 'super
ns-command-modifier 'super
mac-option-modifier 'meta
ns-option-modifier 'meta
;; Free up the right option for character composition
mac-right-option-modifier 'none
ns-right-option-modifier 'none))
(doom--system-windows-p
(setq w32-lwindow-modifier 'super
w32-rwindow-modifier 'super)))
;; HACK: Emacs can't distinguish C-i from TAB, or C-m from RET, in either GUI or
;; TTY frames. This is a byproduct of its history with the terminal, which
;; can't distinguish them either, however, Emacs has separate input events for
;; many contentious keys like TAB and RET (like [tab] and [return], aka
;; "<tab>" and "<return>"), which are only triggered in GUI frames, so here, I
;; create one for C-i. Won't work in TTY frames, though. Doom's :os tty module
;; has a workaround for that though.
(defun doom-init-input-decode-map-h ()
(pcase-dolist (`(,key ,fallback . ,events)
'(([C-i] [?\C-i] tab kp-tab)
([C-m] [?\C-m] return kp-return)))
(define-key
input-decode-map fallback
(cmd! (if (when-let ((keys (this-single-command-raw-keys)))
(and (display-graphic-p)
(not (cl-loop for event in events
if (cl-position event keys)
return t))
;; Use FALLBACK if nothing is bound to KEY, otherwise
;; we've broken all pre-existing FALLBACK keybinds.
(key-binding
(vconcat (if (= 0 (length keys)) [] (cl-subseq keys 0 -1))
key) nil t)))
key fallback)))))
;; `input-decode-map' bindings are resolved on first invokation, and are
;; frame-local, so they must be rebound on every new frame.
(if (daemonp)
(add-hook 'server-after-make-frame-hook #'doom-init-input-decode-map-h)
(doom-init-input-decode-map-h))
;;
;;; Universal, non-nuclear escape
;; `keyboard-quit' is too much of a nuclear option. I wanted an ESC/C-g to
;; do-what-I-mean. It serves four purposes (in order):
;;
;; 1. Quit active states; e.g. highlights, searches, snippets, iedit,
;; multiple-cursors, recording macros, etc.
;; 2. Close popup windows remotely (if it is allowed to)
;; 3. Refresh buffer indicators, like diff-hl and flycheck
;; 4. Or fall back to `keyboard-quit'
;;
;; And it should do these things incrementally, rather than all at once. And it
;; shouldn't interfere with recording macros or the minibuffer. This may require
;; you press ESC/C-g two or three times on some occasions to reach
;; `keyboard-quit', but this is much more intuitive.
(defvar doom-escape-hook nil
"A hook run when C-g is pressed (or ESC in normal mode, for evil users).
More specifically, when `doom/escape' is pressed. If any hook returns non-nil,
all hooks after it are ignored.")
(defun doom/escape (&optional interactive)
"Run `doom-escape-hook'."
(interactive (list 'interactive))
(let ((inhibit-quit t))
(cond ((minibuffer-window-active-p (minibuffer-window))
;; quit the minibuffer if open.
(when interactive
(setq this-command 'abort-recursive-edit))
(abort-recursive-edit))
;; Run all escape hooks. If any returns non-nil, then stop there.
((run-hook-with-args-until-success 'doom-escape-hook))
;; don't abort macros
((or defining-kbd-macro executing-kbd-macro) nil)
;; Back to the default
((unwind-protect (keyboard-quit)
(when interactive
(setq this-command 'keyboard-quit)))))))
(global-set-key [remap keyboard-quit] #'doom/escape)
(with-eval-after-load 'eldoc
(eldoc-add-command 'doom/escape))
;;
;;; General + leader/localleader keys
(require 'general)
;; Convenience aliases
(defalias 'define-key! #'general-def)
(defalias 'undefine-key! #'general-unbind)
;; Prevent "X starts with non-prefix key Y" errors except at startup.
(add-hook 'doom-after-modules-init-hook #'general-auto-unbind-keys)
;; HACK: `map!' uses this instead of `define-leader-key!' because it consumes
;; 20-30% more startup time, so we reimplement it ourselves.
(defmacro doom--define-leader-key (&rest keys)
(let (prefix forms wkforms)
(while keys
(let ((key (pop keys))
(def (pop keys)))
(if (keywordp key)
(when (memq key '(:prefix :infix))
(setq prefix def))
(when prefix
(setq key `(general--concat t ,prefix ,key)))
(let* ((udef (cdr-safe (doom-unquote def)))
(bdef (if (general--extended-def-p udef)
(general--extract-def (general--normalize-extended-def udef))
def)))
(unless (eq bdef :ignore)
(push `(define-key doom-leader-map (general--kbd ,key)
,bdef)
forms))
(when-let (desc (cadr (memq :which-key udef)))
(cl-callf2 append
`((which-key-add-key-based-replacements
(general--concat t doom-leader-alt-key ,key)
,desc)
(which-key-add-key-based-replacements
(general--concat t doom-leader-key ,key)
,desc))
wkforms))))))
(macroexp-progn
(append (and wkforms `((after! which-key ,@(nreverse wkforms))))
(nreverse forms)))))
(defmacro define-leader-key! (&rest args)
"Define <leader> keys.
Uses `general-define-key' under the hood, but does not support :states,
:wk-full-keys or :keymaps. Use `map!' for a more convenient interface.
See `doom-leader-key' and `doom-leader-alt-key' to change the leader prefix."
`(general-define-key
:states nil
:wk-full-keys nil
:keymaps 'doom-leader-map
,@args))
(defmacro define-localleader-key! (&rest args)
"Define <localleader> key.
Uses `general-define-key' under the hood, but does not support :major-modes,
:states, :prefix or :non-normal-prefix. Use `map!' for a more convenient
interface.
See `doom-localleader-key' and `doom-localleader-alt-key' to change the
localleader prefix."
(if (modulep! :editor evil)
;; :non-normal-prefix doesn't apply to non-evil sessions (only evil's
;; emacs state)
`(general-define-key
:states '(normal visual motion emacs insert)
:major-modes t
:prefix doom-localleader-key
:non-normal-prefix doom-localleader-alt-key
,@args)
`(general-define-key
:major-modes t
:prefix doom-localleader-alt-key
,@args)))
;; PERF: We use a prefix commands instead of general's
;; :prefix/:non-normal-prefix properties because general is incredibly slow
;; binding keys en mass with them in conjunction with :states -- an effective
;; doubling of Doom's startup time!
(define-prefix-command 'doom/leader 'doom-leader-map)
(define-key doom-leader-map [override-state] 'all)
;; Bind `doom-leader-key' and `doom-leader-alt-key' as late as possible to give
;; the user a chance to modify them.
(add-hook! 'doom-after-init-hook
(defun doom-init-leader-keys-h ()
"Bind `doom-leader-key' and `doom-leader-alt-key'."
(let ((map general-override-mode-map))
(if (not (featurep 'evil))
(progn
(cond ((equal doom-leader-alt-key "C-c")
(set-keymap-parent doom-leader-map mode-specific-map))
((equal doom-leader-alt-key "C-x")
(set-keymap-parent doom-leader-map ctl-x-map)))
(define-key map (kbd doom-leader-alt-key) 'doom/leader))
(evil-define-key* doom-leader-key-states map (kbd doom-leader-key) 'doom/leader)
(evil-define-key* doom-leader-alt-key-states map (kbd doom-leader-alt-key) 'doom/leader))
;; HACK: When `doom-localleader-key' is a sub-key of `doom-leader-key'
;; (e.g. "S m" under "S"), intermediate minor-mode keymaps (like
;; evil-snipe binding "S" to a command) can block Emacs' prefix key
;; merging, preventing localleader bindings in mode aux keymaps from
;; being reached. Ensure the sub-key is a prefix in `doom-leader-map'
;; so the lookup always succeeds through the leader path.
(when (and doom-localleader-key doom-leader-key
(string-prefix-p (concat doom-leader-key " ")
doom-localleader-key))
(let ((sub-key (substring doom-localleader-key
(1+ (length doom-leader-key)))))
(unless (lookup-key doom-leader-map (kbd sub-key))
(define-key doom-leader-map (kbd sub-key) (make-sparse-keymap)))))
(general-override-mode +1))))
;;
;;; Packages
(use-package! which-key
:hook (doom-first-input . which-key-mode)
:init
(setq which-key-sort-order #'which-key-key-order-alpha
which-key-sort-uppercase-first nil
which-key-add-column-padding 1
which-key-max-display-columns nil
which-key-min-display-lines 6
which-key-side-window-slot -10)
:config
(put 'which-key-replacement-alist 'initial-value which-key-replacement-alist)
(add-hook! 'doom-before-reload-hook
(defun doom-reset-which-key-replacements-h ()
(setq which-key-replacement-alist (get 'which-key-replacement-alist 'initial-value))))
;; general improvements to which-key readability
(which-key-setup-side-window-bottom)
(setq-hook! 'which-key-init-buffer-hook line-spacing 3)
(which-key-add-key-based-replacements doom-leader-key "<leader>")
(which-key-add-key-based-replacements doom-localleader-key "<localleader>"))
;;
;;; `map!' macro
(defvar doom-evil-state-alist
'((?n . normal)
(?v . visual)
(?i . insert)
(?e . emacs)
(?o . operator)
(?m . motion)
(?r . replace)
(?g . global))
"A list of cons cells that map a letter to a evil state symbol.")
(defun doom--map-keyword-to-states (keyword)
"Convert a KEYWORD into a list of evil state symbols.
For example, :nvi will map to (list 'normal 'visual 'insert). See
`doom-evil-state-alist' to customize this."
(cl-loop for l across (doom-keyword-name keyword)
if (assq l doom-evil-state-alist) collect (cdr it)
else do (error "not a valid state: %s" l)))
;; specials
(defvar doom--map-forms nil)
(defvar doom--map-fn nil)
(defvar doom--map-batch-forms nil)
(defvar doom--map-state '(:dummy t))
(defvar doom--map-parent-state nil)
(defvar doom--map-evil-p nil)
(after! evil (setq doom--map-evil-p t))
(defun doom--map-process (rest)
(let ((doom--map-fn doom--map-fn)
doom--map-state
doom--map-forms
desc)
(while rest
(let ((key (pop rest)))
(cond ((listp key)
(doom--map-nested nil key))
((keywordp key)
(pcase key
(:leader
(doom--map-commit)
(setq doom--map-fn 'doom--define-leader-key))
(:localleader
(doom--map-commit)
(setq doom--map-fn 'define-localleader-key!))
(:after
(doom--map-nested (list 'after! (pop rest)) rest)
(setq rest nil))
(:desc
(setq desc (pop rest)))
(:map
(doom--map-set :keymaps `(backquote ,(ensure-list (pop rest)))))
(:mode
(push (cl-loop for m in (ensure-list (pop rest))
collect (intern (concat (symbol-name m) "-map")))
rest)
(push :map rest))
((or :when :unless)
(doom--map-nested (list (intern (doom-keyword-name key)) (pop rest)) rest)
(setq rest nil))
(:prefix-map
(cl-destructuring-bind (prefix . desc)
(let ((arg (pop rest)))
(if (consp arg) arg (list arg)))
(let ((keymap (intern (format "doom-leader-%s-map" desc))))
(setq rest
(append (list :desc desc prefix keymap
:prefix prefix)
rest))
(push `(defvar ,keymap (make-sparse-keymap))
doom--map-forms))))
(:prefix
(cl-destructuring-bind (prefix . desc)
(let ((arg (pop rest)))
(if (consp arg) arg (list arg)))
(doom--map-set (if doom--map-fn :infix :prefix)
prefix)
(when (stringp desc)
(setq rest (append (list :desc desc "" nil) rest)))))
(:textobj
(let* ((key (pop rest))
(inner (pop rest))
(outer (pop rest)))
(push `(map! (:map evil-inner-text-objects-map ,key ,inner)
(:map evil-outer-text-objects-map ,key ,outer))
doom--map-forms)))
(_
(condition-case _
(doom--map-def (pop rest) (pop rest)
(doom--map-keyword-to-states key)
desc)
(error
(error "Not a valid `map!' property: %s" key)))
(setq desc nil))))
((doom--map-def key (pop rest) nil desc)
(setq desc nil)))))
(doom--map-commit)
(macroexp-progn (nreverse (delq nil doom--map-forms)))))
(defun doom--map-append-keys (prop)
(let ((a (plist-get doom--map-parent-state prop))
(b (plist-get doom--map-state prop)))
(if (and a b)
`(general--concat t ,a ,b)
(or a b))))
(defun doom--map-nested (wrapper rest)
(doom--map-commit)
(let ((doom--map-parent-state (doom--map-state)))
(push (if wrapper
(append wrapper (list (doom--map-process rest)))
(doom--map-process rest))
doom--map-forms)))
(defun doom--map-set (prop &optional value)
(unless (equal (plist-get doom--map-state prop) value)
(doom--map-commit))
(setq doom--map-state (plist-put doom--map-state prop value)))
(defun doom--map-def (key def &optional states desc)
(when (or (memq 'global states)
(null states))
(setq states (cons 'nil (delq 'global states))))
(when desc
(let (unquoted)
(cond ((and (listp def)
(keywordp (car-safe (setq unquoted (doom-unquote def)))))
(setq def (list 'quote (plist-put unquoted :which-key desc))))
((setq def (cons 'list
(if (and (equal key "")
(null def))
`(:ignore t :which-key ,desc)
(plist-put (general--normalize-extended-def def)
:which-key desc))))))))
(dolist (state states)
(push (list key def)
(alist-get state doom--map-batch-forms)))
t)
(defun doom--map-commit ()
(when doom--map-batch-forms
(cl-loop with attrs = (doom--map-state)
for (state . defs) in doom--map-batch-forms
if (or doom--map-evil-p (not state))
collect `(,(or doom--map-fn 'general-define-key)
,@(if state `(:states ',state)) ,@attrs
,@(mapcan #'identity (nreverse defs)))
into forms
finally do (push (macroexp-progn forms) doom--map-forms))
(setq doom--map-batch-forms nil)))
(defun doom--map-state ()
(let ((plist
(append (list :prefix (doom--map-append-keys :prefix)
:infix (doom--map-append-keys :infix)
:keymaps
(append (plist-get doom--map-parent-state :keymaps)
(plist-get doom--map-state :keymaps)))
doom--map-state
nil))
newplist)
(while plist
(let ((key (pop plist))
(val (pop plist)))
(when (and val (not (plist-member newplist key)))
(push val newplist)
(push key newplist))))
newplist))
;;
(defmacro map! (&rest rest)
"A convenience macro for defining keybinds, powered by `general'.
If evil isn't loaded, evil-specific bindings are ignored.
Properties
:leader [...] an alias for (:prefix doom-leader-key ...)
:localleader [...] bind to localleader; requires a keymap
:mode [MODE(s)] [...] inner keybinds are applied to major MODE(s)
:map [KEYMAP(s)] [...] inner keybinds are applied to KEYMAP(S)
:prefix [PREFIX] [...] set keybind prefix for following keys. PREFIX
can be a cons cell: (PREFIX . DESCRIPTION)
:prefix-map [PREFIX] [...] same as :prefix, but defines a prefix keymap
where the following keys will be bound. DO NOT
USE THIS IN YOUR PRIVATE CONFIG.
:after [FEATURE] [...] apply keybinds when [FEATURE] loads
:textobj KEY INNER-FN OUTER-FN define a text object keybind pair
:when [CONDITION] [...]
:unless [CONDITION] [...]
Any of the above properties may be nested, so that they only apply to a
certain group of keybinds.
States
:n normal
:v visual
:i insert
:e emacs
:o operator
:m motion
:r replace
:g global (binds the key without evil `current-global-map')
These can be combined in any order, e.g. :nvi will apply to normal, visual and
insert mode. The state resets after the following key=>def pair. If states are
omitted the keybind will be global (no emacs state; this is different from
evil's Emacs state and will work in the absence of `evil-mode').
These must be placed right before the key string.
Do
(map! :leader :desc \"Description\" :n \"C-c\" #'dosomething)
Don't
(map! :n :leader :desc \"Description\" \"C-c\" #'dosomething)
(map! :leader :n :desc \"Description\" \"C-c\" #'dosomething)"
(when (or (bound-and-true-p byte-compile-current-file)
(not noninteractive))
(doom--map-process rest)))
(provide 'doom-keybinds)
;;; doom-keybinds.el ends here