;;; init.el --- Minimal Emacs: built-ins + evil + consult -*- lexical-binding: t; -*- ;; Learning: F1 k KEY, F1 f FUNCTION, F1 v VARIABLE, ;; F1 m (modes), F1 b (bindings). ;; C-h works too, except in normal/motion state where it moves ;; to the window on the left. ;;; Code: ;;;; 1. Basics (let ((bin (expand-file-name "~/.local/bin"))) (add-to-list 'exec-path bin) (setenv "PATH" (concat bin path-separator (getenv "PATH")))) ;; Custom's output goes to its own file, not here. (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) (load custom-file 'noerror 'nomessage) ;; Package archives: GNU and NonGNU are built in, MELPA is added at lowest priority. (require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) (setq package-archive-priorities '(("gnu" . 3) ("nongnu" . 2) ("melpa" . 1))) (package-initialize) (require 'use-package) (setq use-package-always-ensure t) ;;;; 2. UI (menu-bar-mode -1) (tool-bar-mode -1) (scroll-bar-mode -1) (blink-cursor-mode -1) (setq inhibit-startup-screen t initial-scratch-message nil ring-bell-function #'ignore use-short-answers t scroll-conservatively 101 scroll-margin 15) (setq-default indent-tabs-mode nil tab-width 4) (global-hl-line-mode 1) (global-display-line-numbers-mode 1) (pixel-scroll-precision-mode 1) (column-number-mode 1) (electric-pair-mode 1) (which-key-mode 1) ;; Jellybeans theme. (add-to-list 'warning-suppress-types '(files)) (use-package jbeans-theme :config (load-theme 'jbeans t)) ;; Font, on a darker background than the theme's. (set-face-attribute 'default nil :family "JetBrainsMono Nerd Font" :height 120 :background "#131313") ;; Mode line at the top of each window: the header line takes over its ;; format and look, on the editor background. (setq-default header-line-format mode-line-format mode-line-format nil) (set-face-attribute 'header-line nil :inherit 'mode-line :background "#131313") (setq-default line-spacing '(4 . 4)) ;; Command launcher. (keymap-global-set "C-S-p" #'execute-extended-command) ;; Escape cancels prompts. keyboard-quit can't leave the minibuffer, so there ;; Escape runs abort-minibuffers, as C-g does. (keymap-global-set "" #'keyboard-quit) (keymap-set minibuffer-local-map "" #'abort-minibuffers) (add-to-list 'default-frame-alist '(fullscreen . maximized)) ;;;; 3. Files (dolist (d '("backups/" "auto-save/")) (make-directory (locate-user-emacs-file d) t)) (setq backup-directory-alist `(("." . ,(locate-user-emacs-file "backups/"))) auto-save-file-name-transforms `((".*" ,(locate-user-emacs-file "auto-save/") t)) create-lockfiles nil confirm-kill-processes nil vc-follow-symlinks t) (save-place-mode 1) (savehist-mode 1) (recentf-mode 1) (global-auto-revert-mode 1) ;;;; 4. Completion ;; C-n/C-p move through candidates: Vertico remaps next-line/previous-line, ;; and section 5 stops Evil from taking those keys in the minibuffer. (use-package vertico :init (vertico-mode 1) :custom (vertico-count 15)) (setq completion-styles '(basic partial-completion flex) completion-ignore-case t read-buffer-completion-ignore-case t read-file-name-completion-ignore-case t completions-format 'one-column isearch-lazy-count t xref-search-program 'ripgrep) (use-package vertico-posframe :after vertico :custom (vertico-posframe-poshandler #'posframe-poshandler-frame-center) :config (vertico-posframe-mode 1)) ;;;; 5. Evil ;; Evil 1.15.0 on Emacs 31 reads this legacy variable, which Emacs no longer defines. (defvar evil-mode-buffers nil) (defun my/insert-tab () "Insert a literal tab character." (interactive) (insert "\t")) (use-package evil :init ;; evil-want-keybinding: Evil Collection (below) supplies mode bindings instead ;; of Evil's small built-in set. It has to be set before Evil loads. (setq evil-want-C-u-scroll t evil-undo-system 'undo-redo evil-want-keybinding nil) :config (evil-mode 1) (evil-define-key 'insert 'global ;; Literal tab. as well, or a mode's own binding wins in GUI frames. (kbd "TAB") #'my/insert-tab (kbd "") #'my/insert-tab ;; C-SPC: complete using the buffer's completion sources. (kbd "C-SPC") #'completion-at-point (kbd "C-@") #'completion-at-point) ;; Global navigation. ;; C-p, C-f and C-b are assigned to Consult below. (evil-define-key '(normal motion) 'global (kbd "C-e") #'dired-jump)) ;; Evil Collection: vim-style bindings for other packages, one mode at a time. ;; Info here (F1 i); Magit, Magit sections and commit messages in git.el. (use-package evil-collection :after evil :custom ;; Don't add its global [b ]b [e ]e ... bindings. (evil-collection-want-unimpaired-p nil) :config (evil-collection-init '(info))) ;; Minibuffer. Vertico (with-eval-after-load 'vertico (evil-define-key '(insert normal) vertico-map (kbd "C-n") #'vertico-next (kbd "C-p") #'vertico-previous (kbd "TAB") #'vertico-insert (kbd "") #'vertico-insert)) (evil-define-key '(insert normal) minibuffer-local-map (kbd "") #'abort-minibuffers) ;; Window navigation (normal/motion state): Ctrl+hjkl move, Ctrl+` closes. ;; GUI only for C-`: terminals send it as C-@. (evil-define-key '(normal motion) 'global (kbd "C-h") #'windmove-left (kbd "C-j") #'windmove-down (kbd "C-k") #'windmove-up (kbd "C-l") #'windmove-right (kbd "C-`") #'delete-window) ;; Result buffers: hjkl motions and RET to jump to a result. (dolist (m '(occur-mode grep-mode xref--xref-buffer-mode)) (evil-set-initial-state m 'motion)) (with-eval-after-load 'compile (evil-define-key 'motion compilation-mode-map (kbd "RET") #'compile-goto-error)) (with-eval-after-load 'grep (evil-define-key 'motion grep-mode-map (kbd "RET") #'compile-goto-error)) (with-eval-after-load 'replace (evil-define-key 'motion occur-mode-map (kbd "RET") #'occur-mode-goto-occurrence)) (with-eval-after-load 'xref (evil-define-key 'motion xref--xref-buffer-mode-map (kbd "RET") #'xref-goto-xref)) ;;;; 6. Consult ;; Requires the external `fd` and `rg` executables. Both honour .ignore files, ;; so keep excludes (node_modules, elpa, ...) in ~/.ignore or per-project .ignore. ;; Searches start at the project root when there is one, else the current directory. (use-package consult :config ;; Hide noise from the buffer picker. (dolist (re '("\\`\\*scratch\\*\\'" "\\`\\*Messages\\*\\'" "\\`\\*Warnings\\*\\'" "\\`\\*xref\\*\\'")) (add-to-list 'consult-buffer-filter re)) ;; Don't re-run the preview on every keystroke while typing a search. (consult-customize consult-ripgrep :preview-key '(:debounce 0.4 any))) ;; Bind immediately, without waiting for consult to load. (evil-define-key '(normal motion) 'global (kbd "C-p") #'consult-fd (kbd "C-f") #'consult-ripgrep (kbd "C-b") #'consult-buffer) ;;;; 7. Dired (setq dired-dwim-target t dired-kill-when-opening-new-dired-buffer t dired-listing-switches "-agho --group-directories-first" dired-recursive-copies 'always delete-by-moving-to-trash t) (with-eval-after-load 'dired (evil-set-initial-state 'dired-mode 'motion) (evil-define-key 'motion dired-mode-map "h" #'dired-up-directory "l" #'dired-find-file (kbd "RET") #'dired-find-file)) ;;;; 8. Build (setq compile-command "./build.sh" compilation-read-command nil compilation-ask-about-save nil compilation-scroll-output t) (add-hook 'compilation-filter-hook #'ansi-color-compilation-filter) ;; Compilation buffer always at the bottom, full frame width. (add-to-list 'display-buffer-alist '((derived-mode . compilation-mode) (display-buffer-reuse-window display-buffer-at-bottom) (window-height . 0.45))) ;; C-c C-c builds from the project root. ;; C-c b asks first: the prompt is pre-filled with ./build.sh, so type the args after it. (defun my/project-compile-prompt () "Run `project-compile', prompting to edit the command." (interactive) (let ((compilation-read-command t)) (call-interactively #'project-compile))) (keymap-global-set "C-c C-c" #'project-compile) (keymap-global-set "C-c b" #'my/project-compile-prompt) ;; Focus the compilation window when a build starts. (add-hook 'compilation-start-hook (lambda (proc) (when-let* ((win (get-buffer-window (process-buffer proc)))) (select-window win)))) ;;;; 9. Languages ;; Setup for dev languages, modes, lsp and lsp keybinds. (load (locate-user-emacs-file "development.el") nil 'nomessage) ;;;; 10. Git ;; Magit and the :git command live in git.el, next to this file. (load (locate-user-emacs-file "git.el") nil 'nomessage) ;;; init.el ends here