;;; 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 nil: When using ex commands on a visual selection, pass the ;; precise region selected to the command. ;; - If non-nil: Pass the region of /lines/ spanned by the visual ;; selection. evil-ex-visual-char-range nil ;; 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)))))) (defun syd-evil-collection-disable-blacklist-a (fn) (let (evil-collection-key-blacklist) (funcall-interactively fn))) ;; Allow binding to ESC. (advice-add #'evil-collection-vterm-toggle-send-escape :around #'syd-evil-collection-disable-blacklist-a) ;; 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" "[" "]" ""))) (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)) (provide 'syd-evil)