366 lines
16 KiB
EmacsLisp
366 lines
16 KiB
EmacsLisp
;;; syd-evil.el -*- lexical-binding: t; -*-
|
|
|
|
;; More sensible undo functionality. Emacs' default is very weird, not
|
|
;; maintaining a proper history.
|
|
(use-package undo-fu)
|
|
|
|
;; Vim emulation.
|
|
(use-package evil
|
|
:preface
|
|
(setq evil-want-minibuffer t
|
|
evil-move-beyond-eol t
|
|
evil-respect-visual-line-mode t
|
|
evil-vsplit-window-right t
|
|
evil-ex-search-vim-style-regexp t
|
|
evil-want-Y-yank-to-eol t
|
|
evil-want-C-u-scroll t
|
|
evil-want-C-w-in-emacs-state t
|
|
;; - If non-nil: When using ex commands on a visual selection, pass the
|
|
;; precise region selected to the command.
|
|
;; - If nil: Pass the region of /lines/ spanned by the visual selection.
|
|
evil-ex-visual-char-range t
|
|
evil-v$-excludes-newline t
|
|
;; Don't display the state in the mode line.
|
|
evil-mode-line-format nil
|
|
evil-normal-state-cursor 'box
|
|
evil-emacs-state-cursor 'hbar
|
|
evil-operator-state-cursor 'evil-half-cursor
|
|
evil-insert-state-cursor 'bar
|
|
evil-visual-state-cursor 'hollow
|
|
;; Only do highlighting in selected window so that Emacs has less work
|
|
;; to do highlighting them all.
|
|
evil-ex-interactive-search-highlight 'selected-window
|
|
;; It's infuriating that innocuous "beginning of line" or "end of line"
|
|
;; errors will abort macros, so we suppress them:
|
|
evil-kbd-macro-suppress-motion-error t
|
|
evil-undo-system (cond ((featurep 'undo-tree) 'undo-tree)
|
|
((featurep 'undo-fu) 'undo-fu)))
|
|
;; These two are required for evil-collection.
|
|
(setq evil-want-keybinding nil
|
|
evil-want-integration t)
|
|
|
|
:config
|
|
;; 'M-:' starts off in insert mode, yet the normal mode cursor lingers until a
|
|
;; refresh is forced. Quick fix! }:P
|
|
(add-hook 'minibuffer-setup-hook #'evil-refresh-cursor)
|
|
|
|
;; Unbind 'C-k'. Normally, it inserts digraphs; I have a compose key, and
|
|
;; it's strictly less useful than Emacs' native input methods. It often gets
|
|
;; in the way of buffers with navigation, e.g. scrolling through shell/REPL
|
|
;; history, navigating Vertico completions, etc.
|
|
(keymap-set evil-insert-state-map "C-k" nil)
|
|
|
|
;; In imitation of Vim's :mes[sages] command, define an Evil analogue to show
|
|
;; the echo area.
|
|
(defun syd-evil-messages ()
|
|
(interactive)
|
|
(view-echo-area-messages)
|
|
(with-current-buffer messages-buffer-name
|
|
(evil-motion-state 1)))
|
|
(evil-ex-define-cmd "mes[sages]" #'syd-evil-messages)
|
|
|
|
;; On ESC, remove highlighted search results.
|
|
(defun syd-evil-nohl-h ()
|
|
"If any Evil Ex search highlightings are active, remove them and return t.
|
|
Otherwise, nil."
|
|
(let ((names '(evil-ex-substitute evil-ex-search)))
|
|
(when (-any #'evil-ex-hl-active-p names)
|
|
(prog1 t (evil-ex-nohighlight)))))
|
|
(add-hook 'syd-escape-hook #'syd-evil-nohl-h)
|
|
|
|
(general-def
|
|
:states 'motion
|
|
"/" #'evil-ex-search-forward
|
|
"?" #'evil-ex-search-backward
|
|
"n" #'evil-ex-search-next
|
|
"N" #'evil-ex-search-previous
|
|
"*" #'evil-ex-search-word-forward)
|
|
|
|
(evil-mode 1))
|
|
|
|
;; A large, community-sourced collection of preconfigured Evil-mode
|
|
;; integrations.
|
|
(use-package evil-collection
|
|
;; :after evil
|
|
;; :defer t
|
|
:custom (evil-collection-setup-minibuffer t)
|
|
:preface
|
|
(defvar evil-collection-key-blacklist)
|
|
(unless noninteractive
|
|
(defvar syd-evil-collection-disabled-list
|
|
'(anaconda-mode buff-menu calc comint company custom eldoc elisp-mode ert
|
|
free-keys helm help image indent kmacro kotlin-mode lispy outline
|
|
replace shortdoc simple slime tab-bar)
|
|
"A list of `evil-collection' modules to ignore. See
|
|
`evil-collection-mode-list' for a list of available options.")
|
|
|
|
;; We do this ourselves.
|
|
(defvar evil-collection-want-unimpaired-p nil)
|
|
;; We binds goto-reference on gD and goto-assignments on gA ourselves
|
|
(defvar evil-collection-want-find-usages-bindings-p nil)
|
|
;; Reduces keybind conflicts between outline-mode and org-mode (which is
|
|
;; derived from outline-mode).
|
|
(defvar evil-collection-outline-enable-in-minor-mode-p nil)
|
|
;; We handle loading evil-collection ourselves
|
|
(defvar evil-collection--supported-modes nil)
|
|
;; This has to be defined here since evil-collection doesn't autoload its own.
|
|
;; It must be updated whenever evil-collection updates theirs.
|
|
(defvar evil-collection-mode-list
|
|
`(2048-game ag alchemist anaconda-mode apropos arc-mode atomic-chrome
|
|
auto-package-update beginend bluetooth bm bookmark
|
|
(buff-menu "buff-menu") bufler calc calendar cider citre cmake-mode
|
|
color-rg comint company compile consult corfu crdt (csv "csv-mode")
|
|
(custom cus-edit) cus-theme dape dashboard daemons deadgrep debbugs
|
|
debug devdocs dictionary diff-hl diff-mode dired dired-sidebar
|
|
disk-usage distel doc-view docker eat ebib ebuku edbi edebug ediff eglot
|
|
elpaca ement explain-pause-mode eldoc elfeed elisp-mode elisp-refs
|
|
elisp-slime-nav embark emms ,@(if (> emacs-major-version 28) '(emoji))
|
|
epa ert eshell eval-sexp-fu evil-mc eww fanyi finder flycheck flymake
|
|
forge free-keys geiser ggtags git-timemachine gited gnus go-mode gptel
|
|
grep guix hackernews helm help helpful hg-histedit hungry-delete hyrolo
|
|
ibuffer (image image-mode) image-dired image+ imenu imenu-list
|
|
(indent "indent") indium info ivy js2-mode
|
|
,@(if (>= emacs-major-version 30) '(kmacro)) leetcode lispy lms log-edit
|
|
log-view lsp-ui-imenu lua-mode kotlin-mode macrostep man
|
|
(magit magit-repos magit-submodule) magit-repos magit-section
|
|
magit-todos markdown-mode monky mpc mpdel mu4e mu4e-conversation neotree
|
|
newsticker notmuch nov omnisharp org org-present org-roam osx-dictionary
|
|
outline p4 (package-menu package) pass (pdf pdf-tools) popup proced
|
|
prodigy profiler p-search python quickrun racer racket-describe realgud
|
|
reftex replace restclient rg ripgrep rjsx-mode robe rtags ruby-mode
|
|
scheme scroll-lock selectrum sh-script
|
|
,@(if (> emacs-major-version 27) '(shortdoc)) simple simple-mpc slime
|
|
sly smerge-mode snake so-long speedbar tab-bar tablist tar-mode telega
|
|
(term term ansi-term multi-term) tetris thread tide timer-list
|
|
transmission trashed tuareg typescript-mode vc-annotate vc-dir vc-git
|
|
vdiff vertico view vlf vterm vundo w3m wdired wgrep which-key
|
|
with-editor woman xref xwidget yaml-mode youtube-dl zmusic
|
|
(ztree ztree-diff)))
|
|
|
|
(cl-defun syd-evil-collection-init (module &key disabled-modules)
|
|
"Initialise evil-collection-MODULE.
|
|
|
|
A wrapper for `evil-collection-init' that respects a given list of disabled
|
|
modules."
|
|
(let ((module* (or (car-safe module) module)))
|
|
(unless (memq module* disabled-modules)
|
|
(message "Loading evil-collection-%s%s"
|
|
module*
|
|
(if after-init-time "" " too early! }:("))
|
|
(with-demoted-errors "error loading evil-collection: %s"
|
|
(evil-collection-init (list module))))))
|
|
|
|
;; Allow binding to ESC.
|
|
(syd-defadvice syd-evil-collection-disable-blacklist-a (fn)
|
|
:around #'evil-collection-vterm-toggle-send-escape
|
|
(let (evil-collection-key-blacklist)
|
|
(funcall-interactively fn)))
|
|
|
|
;; These modes belong to packages that Emacs always loads at startup, causing
|
|
;; evil-collection and it's co-packages to all load immediately. We avoid
|
|
;; this by loading them after evil-collection has first loaded...
|
|
(with-eval-after-load 'evil-collection
|
|
(require 'syd-prelude)
|
|
(require 'syd-keybinds)
|
|
;; Don't let evil-collection interfere with certain keys
|
|
(setq evil-collection-key-blacklist
|
|
(append (list syd-leader-key syd-localleader-key
|
|
syd-alt-leader-key)
|
|
evil-collection-key-blacklist
|
|
;; Reserved for goto definition; lookup docs; eval; eval
|
|
;; buffer; movement prefix; movement prefix; escaping };).
|
|
'("gd" "K" "gr" "gR" "[" "]" "<escape>")))
|
|
|
|
(mapc #'syd-evil-collection-init '(comint custom))
|
|
|
|
(with-eval-after-load 'evil
|
|
;; Emacs loads these two packages immediately, at startup, which needlessly
|
|
;; convolutes load order for evil-collection-help.
|
|
(with-transient-after 'help-mode
|
|
(syd-evil-collection-init 'help))
|
|
(with-transient-after 'Buffer-menu-mode
|
|
(syd-evil-collection-init '(buff-menu "buff-menu")))
|
|
(with-transient-after 'calc-mode
|
|
(syd-evil-collection-init 'calc))
|
|
(with-transient-after 'image-mode
|
|
(syd-evil-collection-init 'image))
|
|
(with-transient-after 'emacs-lisp-mode
|
|
(syd-evil-collection-init 'elisp-mode))
|
|
(with-transient-after 'occur-mode
|
|
(syd-evil-collection-init 'replace))
|
|
(with-transient-after 'indent-rigidly
|
|
(syd-evil-collection-init '(indent "indent")))
|
|
(when (>= emacs-major-version 30)
|
|
(with-transient-after 'kmacro-menu-mode
|
|
(syd-evil-collection-init 'kmacro)))
|
|
(with-transient-after 'minibuffer-setup-hook
|
|
(when evil-collection-setup-minibuffer
|
|
(syd-evil-collection-init 'minibuffer)
|
|
(evil-collection-minibuffer-insert)))
|
|
(with-transient-after 'process-menu-mode
|
|
(syd-evil-collection-init '(process-menu simple)))
|
|
(with-transient-after 'shortdoc-mode
|
|
(syd-evil-collection-init 'shortdoc))
|
|
(with-transient-after 'tabulated-list-mode
|
|
(syd-evil-collection-init 'tabulated-list))
|
|
(with-transient-after 'tab-bar-mode
|
|
(syd-evil-collection-init 'tab-bar))
|
|
|
|
;; HACK: Do this ourselves because evil-collection break's
|
|
;; `eval-after-load' load order by loading their target plugin before
|
|
;; applying keys. This makes it hard for end-users to overwrite these
|
|
;; keybinds with a simple `after!' or `with-eval-after-load'.
|
|
(dolist (mode evil-collection-mode-list)
|
|
(dolist (req (or (cdr-safe mode) (list mode)))
|
|
(with-eval-after-load req
|
|
(syd-evil-collection-init
|
|
mode
|
|
:disabled-modules syd-evil-collection-disabled-list))))))))
|
|
|
|
;; Tim Pope's famous `surround.vim' for Evil.
|
|
(use-package evil-surround
|
|
:commands (global-evil-surround-mode
|
|
evil-surround-edit
|
|
evil-Surround-edit
|
|
evil-surround-region)
|
|
:hook (on-first-input . global-evil-surround-mode)
|
|
:config
|
|
;; In `emacs-lisp-mode', `' is a much more common pair than ``.
|
|
(add-hook 'emacs-lisp-mode-hook
|
|
(lambda ()
|
|
(push '(?` . ("`" . "'")) evil-surround-pairs-alist))))
|
|
|
|
;; TODO: I'd like JK to escape visual state. evil-escape only allows defining a
|
|
;; single key sequence. Perhaps key-chord is capable of this?
|
|
(use-package evil-escape
|
|
:hook (on-first-input . evil-escape-mode)
|
|
:custom ((evil-escape-key-sequence "jk")
|
|
(evil-escape-excluded-states '(normal visual multiedit emacs motion))
|
|
(evil-escape-delay 0.15)))
|
|
|
|
;; `evil-nerd-commenter' has a bunch of cool functions[1]. Here, only the Evil
|
|
;; operator is used. }:3
|
|
;; [1]: https://github.com/redguardtoo/evil-nerd-commenter?tab=readme-ov-file#commands-and-hotkeys
|
|
(use-package evil-nerd-commenter
|
|
:commands (evilnc-comment-operator
|
|
evilnc-inner-comment
|
|
evilnc-outer-commenter)
|
|
:defer t
|
|
:bind (:map evil-normal-state-map ("#" . evilnc-comment-operator)
|
|
:map evil-visual-state-map ("#" . evilnc-comment-operator)
|
|
:map evil-inner-text-objects-map ("c" . evilnc-inner-comment)
|
|
:map evil-outer-text-objects-map ("c" . evilnc-outer-comment)))
|
|
|
|
;; Enhance `evil-surround' with integration with `embrace'.
|
|
(use-package evil-embrace
|
|
:disabled
|
|
:after evil-surround
|
|
:config
|
|
(evil-embrace-enable-evil-surround-integration))
|
|
|
|
;; Provides an Evil operator to swap two spans of text.
|
|
(use-package evil-exchange
|
|
:bind (:map evil-normal-state-map ("gX" . evil-exchange)
|
|
:map evil-visual-state-map ("gX" . evil-exchange)))
|
|
|
|
;; Evil doesn't ship with support for Vim's 'g-'/'g+'. `evil-numbers'
|
|
;; implements this.
|
|
(use-package evil-numbers
|
|
;; 'g=' is a bit more comfortable than 'g+', whilst preserving the analogy.
|
|
;; ('=' is '+' modulo shift)
|
|
:bind (:map evil-normal-state-map ("g=" . 'evil-numbers/inc-at-pt)
|
|
:map evil-normal-state-map ("g-" . 'evil-numbers/dec-at-pt))
|
|
:defer t)
|
|
|
|
;; Tree-sitter queries → Evil text objects.
|
|
(use-package evil-textobj-tree-sitter
|
|
:defer t)
|
|
|
|
;; Visually "flash" the region acted upon by Evil-mode operations.
|
|
(use-package evil-goggles
|
|
:hook (on-first-input . evil-goggles-mode)
|
|
;; The flash animation will delay actions, which can be very annoying for some
|
|
;; operations. Disable `evil-goggles' for those ones.
|
|
:custom
|
|
((evil-goggles-enable-delete nil)
|
|
(evil-goggles-enable-change nil)
|
|
(evil-goggles-duration 0.1)))
|
|
|
|
;; Change cursor shape and color by evil state in terminal.
|
|
(use-package evil-terminal-cursor-changer
|
|
;; This package is only useful in the terminal.
|
|
:if (not (display-graphic-p))
|
|
:defer t
|
|
:hook (on-first-input . evil-terminal-cursor-changer-activate))
|
|
|
|
;; Automatic alignment in region, by regexp.
|
|
(use-package evil-lion
|
|
:hook (on-first-input . evil-lion-mode))
|
|
|
|
;; 'g' text object selecting the entire buffer.
|
|
(with-eval-after-load 'evil
|
|
(evil-define-text-object
|
|
evil-entire-buffer (count &optional beg end type)
|
|
"Select entire buffer"
|
|
(evil-range (point-min) (point-max) type))
|
|
(define-key evil-inner-text-objects-map "g" #'evil-entire-buffer)
|
|
(define-key evil-outer-text-objects-map "g" #'evil-entire-buffer))
|
|
|
|
;; 2-character search.
|
|
(use-package evil-snipe
|
|
:commands (evil-snipe-local-mode evil-snipe-override-local-mode)
|
|
:hook ((on-first-input . evil-snipe-override-mode)
|
|
;; (on-first-input . evil-snipe-mode)
|
|
)
|
|
:custom ((evil-snipe-smart-case t)
|
|
(evil-snipe-scope 'visible)
|
|
(evil-snipe-repeat-scope 'visible)
|
|
(evil-snipe-char-fold t)))
|
|
|
|
;; Evil's default behaviour for '#'/'*' in visual state will remain in visual
|
|
;; mode, and jump to the next occurence of the symbol under point. That is, the
|
|
;; movement is exactly the same as it is in normal state; if the region is over
|
|
;; the text `two words`, but the point is over `two`, Evil will search for
|
|
;; `two`. `evil-visualstar' will instead search for `two words`.
|
|
(use-package evil-visualstar
|
|
:defer t
|
|
:bind (:map evil-visual-state-map
|
|
("*" . evil-visualstar/begin-search-forward)))
|
|
|
|
(defvar syd-evil-last-eval-expression-register ?e
|
|
"An Evil-mode register in which the last expression evaluated with an
|
|
interactive call to `eval-expression' is stored.")
|
|
|
|
(with-eval-after-load 'evil
|
|
(defun syd-set-eval-expression-register-a (expr &rest _)
|
|
"If called interactively, set the register
|
|
`syd-evil-last-eval-expression-register' to a printed form of EXPR."
|
|
(when (called-interactively-p 'interactive)
|
|
(->> (pp-to-string expr)
|
|
(string-remove-suffix "\n")
|
|
(evil-set-register syd-evil-last-eval-expression-register))))
|
|
(advice-add #'eval-expression
|
|
:after #'syd-set-eval-expression-register-a))
|
|
|
|
;; HACK: '=' unpredictably moves the cursor when it really doesn't need to.
|
|
(defun syd-evil-dont-move-point-a (fn &rest args)
|
|
"Used as :around advice on Evil operators to avoid moving the point."
|
|
;; We don't use `save-excursion', as we /only/ want to restore the point.
|
|
(save-excursion (apply fn args)))
|
|
|
|
(with-eval-after-load 'evil
|
|
(advice-add #'evil-indent
|
|
:around #'syd-evil-dont-move-point-a))
|
|
|
|
;(use-package evil-leap
|
|
; :hook (on-first-input . evil-leap-mode)
|
|
; :load-path "/home/crumb/src/evil-leap"
|
|
; :straight nil
|
|
; ;; :straight (:type git
|
|
; ;; :host gitlab
|
|
; ;; :repo "msyds/evil-leap")
|
|
; :config
|
|
; (evil-leap-install-default-keybindings))
|
|
|
|
(provide 'syd-evil)
|