;;; init.el --- Emacs configuration file -*- lexical-binding: t; -*- ;; Author: Jessie Hildebrandt ;; Homepage: https://gitlab.com/jessieh/dot-emacs ;; Package-Requires: ((emacs "28.1")) ;; This file is not part of GNU Emacs. ;;; Commentary: ;; jessieh's (Mostly) Portable Emacs Config ;; ;; This configuration file was designed to work with Emacs 28.1, and will not ;; work with earlier versions. You should probably have native-comp enabled. ;; ;; This file will automatically generate an early-init.el file upon first run, ;; or if early-init.el is missing. ;; ;; All configuration efforts here are organized around the use of wrapped ;; `use-package' macros. The keyword order for any config entry should be: ;; - if/when/unless ;; - demand ;; - after ;; - requires ;; - defines/functions ;; - init/config ;; - custom/custom-faces ;; - commands ;; - mode/interpreter/hook ;; - bind/bind*/bind-keymap/bind-keymap* ;; ;; Custom functions and variables should generally be prefixed with "user/". ;; This prefix is purposefully unidiomatic so as to avoid collisions with any ;; package prefixes, inbuilt or otherwise. ;;; License: ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License as ;; published by the Free Software Foundation; either version 2, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth ;; Floor, Boston, MA 02110-1301, USA. ;;; Code: ;; -------------------------------------------------------------------------- ;; ;; ;; Startup ;; ;; -------------------------------------------------------------------------- ;; (defconst user/init-file (locate-user-emacs-file "init.el") "Location of user init file.") (defconst user/early-init-file (locate-user-emacs-file "early-init.el") "Location of user early-init file.") ;; ---------------------------------- ;; ;; early-init.el file creation ;; ---------------------------------- ;; (defvar user/early-init-forms '(";;; early-init.el --- Early initialization -*- lexical-binding: t; -*-" ";; This file is not part of GNU Emacs." ";;; Commentary:" ";; This file has been automatically generated by init.el." ";; More relevant commentary is likely in init.el, not here." ";;; Code:" (defconst user/gc-cons-threshold (* 32 1024 1024) "Preferred garbage collection threshold value (32MB).") (defconst user/gc-cons-percentage 0.1 "Preferred garbage collection percentage value (10%).") (defun user/defer-garbage-collection () "Defer garbage collection by maximizing the collection threshold." (setq gc-cons-threshold most-positive-fixnum gc-cons-percentage 1.0)) (defun user/restore-garbage-collection () "Restore the garbage collection threshold parameters in a deferred fashion." (setq gc-cons-threshold user/gc-cons-threshold gc-cons-percentage user/gc-cons-percentage)) ";; Defer garbage collection until after initialization" (user/defer-garbage-collection) (add-hook 'emacs-startup-hook #'user/restore-garbage-collection) ";; Clear `file-name-handler-alist' until after initialization" (let ((default-file-name-handler-alist file-name-handler-alist)) (setq file-name-handler-alist nil) (add-hook 'emacs-startup-hook (lambda () (setq file-name-handler-alist default-file-name-handler-alist)))) ";; Configure GUI components before initial frame creation" (setq frame-inhibit-implied-resize t default-frame-alist '((menu-bar-lines . 0) (tool-bar-lines . 0) (vertical-scroll-bars . nil) (background-color . "gray15") (foreground-color . "gray85"))) ";; package.el initialization is handled manually in init.el" (setq package-enable-at-startup nil) ";;; init.el ends here") "List of forms that are written to the user early-init file.") (defun user/write-forms-to-file (forms file) "Format and pretty-print list FORMS to FILE." (with-temp-file file (mapcar (lambda (form) (insert (concat (if (stringp form) (prin1-to-string form :no-escape) (pp-to-string form)) "\n"))) forms))) ;; Create (and load) early-init file if it does not yet exist, or if this file ;; is currently being recompiled. We recreate early-init.el during compilation ;; in case `user/early-init-forms' has been changed (unless (file-exists-p user/early-init-file) (user/write-forms-to-file user/early-init-forms user/early-init-file) (load user/early-init-file nil nil :no-suffix)) ;; Make sure that the user init files are byte-compiled. (let ((byte-compile-warnings nil)) (when (file-newer-than-file-p user/early-init-file (concat user/early-init-file "c")) (byte-compile-file user/early-init-file)) (when (file-newer-than-file-p user/init-file (concat user/init-file "c")) (byte-compile-file user/init-file))) ;; ---------------------------------- ;; ;; Package manager initialization ;; ---------------------------------- ;; ;; To be evaluated at both compile time and run time (eval-and-compile (setq package-user-dir (locate-user-emacs-file "package/") package-native-compile t package-check-signature nil use-package-hook-name-suffix nil use-package-always-demand (daemonp))) ;; To be evaluated only at compile time (eval-when-compile ;; Initialize package.el (require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) (unless (bound-and-true-p package--initialized) (package-initialize)) (package-refresh-contents) ;; Install use-package (unless (package-installed-p 'use-package) (package-install 'use-package)) (require 'use-package) ;; After initialization, build quickstart file containing package autoload defs ;; and compile it alongside any other uncompiled Elisp files in installed packages (add-hook 'emacs-startup-hook (lambda () (let ((byte-compile-warnings nil)) (byte-recompile-directory package-user-dir 0) (package-quickstart-refresh))))) ;; Load "package-quickstart.el" file containing package autoload defs with a ;; call to `package-activate-all'. This is must be done to load any installed ;; packages during initialization outside of compile time because we won't have ;; called `package-initialize'. We also require `bind-key' here, which the ;; compiled `use-package' macros still rely on for handling keybinds (unless (bound-and-true-p package--initialized) (package-activate-all) (require 'bind-key)) ;; -------------------------------------------------------------------------- ;; ;; ;; Custom interactive functions ;; ;; -------------------------------------------------------------------------- ;; (defun user/open-init-file () "Opens the user init file in a new buffer." (interactive) (find-file user/init-file)) (defun user/byte-compile-init-files () "Byte-compile the user init and early-init files." (interactive) (message "Byte-compiling init and early-init file...") (byte-compile-file user/init-file) (byte-compile-file user/early-init-file)) (defun user/refresh-packages () "Refresh packages that have been configured for use in the user init file. This is accomplished by deleting `package-user-dir' and recompiling the user init file, which initializes the package manager during compile time." (interactive) (when (yes-or-no-p "Redownload and refresh packages? ") (message "Refreshing packages...") (delete-directory package-user-dir :recursive) (user/byte-compile-init-files))) (defun user/download-latest-init-file () "Download the latest user init file from jessieh.net/emacs. The user init file will be automatically recompiled after downloading. If `user/init-file' points to a symlink, nothing will be downloaded." (interactive) (if (file-symlink-p user/init-file) (message "%s is a symlink, operation aborted. Please update the init file manually." user/init-file) (when (yes-or-no-p "Download latest init file from jessieh.net/emacs? ") (message "Updating init file...") (url-copy-file "https://jessieh.net/emacs" user/init-file :ok-if-already-exists) (user/byte-compile-init-files)))) (defun user/select-minibuffer-window () "Select the minibuffer window if it is active." (interactive) (when (active-minibuffer-window) (select-window (active-minibuffer-window)))) (defun user/scratch-buffer () "Open the scratch buffer, (re)creating it if not present." (interactive) (pop-to-buffer (get-buffer-create "*scratch*") '((display-buffer-reuse-window display-buffer-same-window))) (lisp-interaction-mode)) (defun user/scan-directory-for-projects () "Prompt for a directory and then scan for any project roots within. Inaccessible directories and .git directories are skipped during searching. When done searching, you will be shown a buffer of all discovered project directories and will be prompted to index them. Choosing to index the project directories will register them with Emacs' project management system." (interactive) (when-let* ((valid-root-p (lambda (path) (and (file-accessible-directory-p path) (not (string= (file-name-nondirectory path) ".git"))))) (directory (read-directory-name "Look for projects in: ")) (project-dir-list (mapcar #'file-name-directory (directory-files-recursively directory "\\.git$" #'valid-root-p t))) (num-projects-found (length project-dir-list)) (temp-buffer-name (concat "*" (number-to-string num-projects-found) " Project Roots Found*"))) (with-output-to-temp-buffer temp-buffer-name (princ (mapconcat #'identity project-dir-list "\n"))) (when (yes-or-no-p "Index all of these directories as projects? ") (unless (fboundp 'project-remember-projects-under) (require 'project)) (mapc #'project-remember-projects-under project-dir-list) (message "%d%s" num-projects-found " directories indexed as projects.")) (with-current-buffer temp-buffer-name (kill-buffer-and-window)))) ;; -------------------------------------------------------------------------- ;; ;; ;; Internal/built-in packages ;; ;; -------------------------------------------------------------------------- ;; (defmacro editor-feature (name docstring &rest args) "Apply NAME and ARGS to `use-package' with `:ensure' defaulted to nil. DOCSTRING is an optional form that is discarded upon expansion." (declare (doc-string 2) (indent defun)) (ignore docstring) `(use-package ,name :ensure nil ,@args)) ;; ---------------------------------- ;; ;; General editor setup ;; ---------------------------------- ;; (defun user/ensure-region-active (func &rest args) "Apply ARGS to FUNC only if `region-active-p' is non-nil." (when (region-active-p) (apply func args))) (editor-feature emacs "General editor-wide configuration" :config ;; Enable upcase/downcase region commands (put 'upcase-region 'disabled nil) (put 'downcase-region 'disabled nil) (advice-add 'upcase-region :around 'user/ensure-region-active) (advice-add 'downcase-region :around 'user/ensure-region-active) :custom ;; Default working directory (default-directory (if (eq system-type 'windows-nt) (setq default-directory (getenv "USERPROFILE")) (setq default-directory "~/"))) ;; General configuration (frame-title-format '("Emacs - %b") "Set frame title to buffer name") (truncate-lines t "Truncate lines instead of wrapping") (message-truncate-lines t "Truncate messages in the echo area") (cursor-in-non-selected-windows nil "Hide cursor in inactive windows") (x-gtk-use-system-tooltips nil "Disable GTK tooltips in favor of in-editor tooltips") (ring-bell-function 'ignore "Disable terminal bell") (max-mini-window-height 10 "Limit minibuffer height to 10 lines") (enable-recursive-minibuffers t "Allow minibuffer commands to be called in the minibuffer") (load-prefer-newer t "Load from source files if they are newer than bytecode files") (server-client-instructions nil "Suppress help messages from the server for new frames") ;; Startup (initial-scratch-message "" "Leave scratch buffer empty on startup") (inhibit-startup-screen t "Do not create or show the initial splash screen") (inhibit-default-init t "Do not attempt to load any OS-provided init files") ;; Default style rules (indent-tabs-mode nil "Use spaces for indentation") (tab-width 4 "Use 4 spaces for indentation") ;; Scrolling (mouse-wheel-progressive-speed nil "Disable mouse wheel acceleration during scrolling") (scroll-preserve-screen-position 1 "Prevent the cursor from moving during scrolling") (scroll-conservatively 101 "Scroll only one line at a time when cursor leaves view") (scroll-margin 5 "Maintain margin of 5 lines around cursor during scrolling") ;; Performance tweaks (redisplay-skip-fontification-on-input t "Improve redisplay performance while scrolling") (fast-but-imprecise-scrolling t "Improve redisplay performance while scrolling") (jit-lock-defer-time 0 "Defer fontification while input is pending") (auto-window-vscroll nil "Prevent calcuation of arbitrary line heights while scrolling") (auto-mode-case-fold nil "Disable case-insensitive second pass over `auto-mode-alist'") ;; Bidirectional text (bidi-inhibit-bpa t "Disable parentheses matching for bidirectional text") (bidi-display-reordering 'left-to-right "Force global left-to-right text direction") (bidi-paragraph-direction 'left-to-right "Disable paragraph directionality detection") ;; Elisp compilation warnings (native-comp-async-report-warnings-errors nil "Don't report errors from async native compilation") (byte-compile-warnings '(not lexical free-vars noruntime unresolved docstrings)) ;; Filter out buffer-incompatible interactive commands by default (read-extended-command-predicate #'command-completion-default-include-p) :custom-face ;; Default font (default ((t (:family "Fira Code Regular" :height 100)))) :bind ;; General binds ("C-z" . undo) ("C-c SPC" . tmm-menubar) ("C-c q" . visual-line-mode) ("C-c d" . delete-pair) ("C-c r" . revert-buffer-quick) ("C-c o" . user/select-minibuffer-window) ("C-c s" . user/scratch-buffer) ;; Navigation ("M-n" . scroll-up-line) ("M-p" . scroll-down-line) ("C-M-n" . forward-paragraph) ("C-M-p" . backward-paragraph) :bind* ;; Window navigation ("C-c C-i" . windmove-up) ("C-c C-k" . windmove-down) ("C-c C-j" . windmove-left) ("C-c C-l" . windmove-right)) ;; ---------------------------------- ;; ;; File save/backup behavior ;; ---------------------------------- ;; (defconst user/custom-file (locate-user-emacs-file "custom.el") "Location of user customizations file.") (defconst user/backup-directory (locate-user-emacs-file "backup/") "Location of user backup directory.") (defconst user/auto-save-directory (locate-user-emacs-file "auto-save/") "Location of user auto save directory.") (defconst user/lock-file-directory (locate-user-emacs-file "lock-file/") "Location of user lock file directory.") (make-directory user/backup-directory :parents) (make-directory user/auto-save-directory :parents) (make-directory user/lock-file-directory :parents) (editor-feature files "File/save backup behavior configuration" :custom ;; Config file (custom-file user/custom-file "Store customization info in a separate file") ;; Directories (backup-directory-alist `((".*" . ,user/backup-directory))) (auto-save-file-name-transforms `((".*" ,user/auto-save-directory t))) (lock-file-name-transforms `((".*" ,user/lock-file-directory t))) ;; Backup behavior (backup-by-copying t "Use copying unconditionally when creating backups") (version-control t "Use version numbers on backup files") (delete-old-versions t "Clean up old backup files") (kept-new-versions 5 "Keep 5 recent backup files") (kept-old-versions 3 "Keep 3 old backup files") :hook (before-save-hook . delete-trailing-whitespace)) ;; ---------------------------------- ;; ;; Graphical frame settings ;; ---------------------------------- ;; (defvar user/initial-frame-created nil "Whether or not the first frame has been initialized by the server.") (defvar user/after-daemon-make-initial-frame-functions nil "Run when the first frame is produced by the server in daemon mode.") (defun user/set-up-frame (frame) "Set up newly-created frame FRAME." (when (display-graphic-p frame) (with-selected-frame frame (let ((winid (frame-parameter frame 'outer-window-id))) (call-process-shell-command (concat "xprop -f _GTK_THEME_VARIANT 8u -set _GTK_THEME_VARIANT dark -id " winid) nil 0)) (when (and (daemonp) (not user/initial-frame-created)) (run-hooks 'user/after-daemon-make-initial-frame-functions) (setq user/initial-frame-created t))))) (editor-feature frame "Graphical frame configuration" :config (mapc #'user/set-up-frame (frame-list)) :hook (after-make-frame-functions . user/set-up-frame)) ;; ---------------------------------- ;; ;; autorevert ;; ---------------------------------- ;; (editor-feature autorevert "Displays on-disk changes to files in unmodified buffers automatically" :config (global-auto-revert-mode)) ;; ---------------------------------- ;; ;; display-line-numbers ;; ---------------------------------- ;; (editor-feature display-line-numbers "Displays the absolute number of each line in a buffer" :custom (display-line-numbers-width-start 100 "Count number of lines (+100) in buffer for initial line number width") :hook (prog-mode-hook . (lambda () (unless (derived-mode-p 'lisp-interaction-mode) (display-line-numbers-mode)))) :bind ("C-c n" . display-line-numbers-mode)) ;; ---------------------------------- ;; ;; elec-pair ;; ---------------------------------- ;; (editor-feature elec-pair "Enables automatic pairing of most paired delimiters" :hook (prog-mode-hook . electric-pair-local-mode)) ;; ---------------------------------- ;; ;; eshell ;; ---------------------------------- ;; (defun user/abbrev-path (path) "Return a PATH with all but the last directory name abbreviated." (let* ((components (split-string (abbreviate-file-name path) "/")) (str "")) (while (cdr components) (setq str (concat str (cond ((= 0 (length (car components))) "/") ((= 1 (length (car components))) (concat (car components) "/")) (t (if (string= "." (string (elt (car components) 0))) (concat (substring (car components) 0 2) "/") (string (elt (car components) 0) ?/))))) components (cdr components))) (concat (propertize str 'face 'font-lock-comment-face) (propertize (cl-reduce (lambda (a b) (concat a "/" b)) components) 'face 'font-lock-doc-face)))) (defun user/open-eshell () "Switch to an active unfocused `eshell' session, or start a new one." (interactive) (let ((eshell-buffer (cl-find-if (lambda (buffer) (eq (buffer-local-value 'major-mode buffer) 'eshell-mode)) (buffer-list)))) (if eshell-buffer (if (eq (current-buffer) eshell-buffer) (eshell :new-session) (pop-to-buffer eshell-buffer '((display-buffer-reuse-window display-buffer-same-window)))) (eshell)))) (editor-feature eshell "Provides a shell-like interpreter that can process shell or lisp commands" :custom (eshell-banner-message '(concat "\n" (if (executable-find "fortune") (shell-command-to-string "fortune -s computers") "Hello, commander."))) (eshell-prompt-function (lambda () (concat "\n" (user/abbrev-path (eshell/pwd)) (if (= (user-uid) 0) " # " " ❱ ")))) (eshell-prompt-regexp "^[^#❱\n]* [#❱] ") (eshell-visual-commands '("fish" "bash" "ssh" "mosh" "fzf" "top" "htop" "less")) (eshell-destroy-buffer-when-process-dies t "Destroys child buffers after their process returns") (eshell-error-if-no-glob t "Produces an error if a glob pattern fails to match, like zsh") (eshell-hist-ignoredups t "Treat multiple repeating history entries as a single entry") :hook (eshell-post-command-hook . (lambda () (rename-buffer (concat eshell-buffer-name " " (user/abbrev-path (eshell/pwd))) :unique))) (eshell-mode-hook . (lambda () (setq-local global-hl-line-mode nil tab-line-tabs-function #'tab-line-tabs-mode-buffers) (tab-line-mode))) :bind ("C-c RET" . user/open-eshell)) ;; `eshell-mode-map' is not available until the `esh-mode' module is loaded, so ;; the local keymap bindings are set up here (editor-feature esh-mode "eshell module that handles input from the user" :bind (:map eshell-mode-map ("C-c RET" . nil) ("C- . eshell-copy-old-input") ("C-c j" . tab-line-switch-to-prev-tab) ("C-c l" . tab-line-switch-to-next-tab))) ;; ---------------------------------- ;; ;; hl-line ;; ---------------------------------- ;; (editor-feature hl-line "Highlights the current line in a buffer" :config (global-hl-line-mode)) ;; ---------------------------------- ;; ;; ispell ;; ---------------------------------- ;; (editor-feature ispell "Checks for spelling errors and suggests corrections from a dictionary" :bind ("C-c ' '" . ispell) ("C-c ' w" . ispell-word) ("C-c ' r" . ispell-region) ("C-c ' b" . ispell-buffer) ("C-c ' ;" . ispell-comment-or-string-at-point) ("C-c ' C-;" . ispell-comments-and-strings)) ;; ---------------------------------- ;; ;; paren ;; ---------------------------------- ;; (editor-feature paren "Highlights matching delimiter pairs under the cursor" :config (show-paren-mode) :custom (show-paren-delay 0.0 "Highlight matching delimiters instantly")) ;; ---------------------------------- ;; ;; savehist ;; ---------------------------------- ;; (editor-feature savehist "Persists minibuffer history between sessions" :config (savehist-mode)) ;; ---------------------------------- ;; ;; so-long ;; ---------------------------------- ;; (editor-feature so-long "Deactivates certain editor features when opening files with very long lines" :config (global-so-long-mode)) ;; ---------------------------------- ;; ;; subword ;; ---------------------------------- ;; (editor-feature subword "Enables detection of subwords as words in camel case or pascal case names" :config (global-subword-mode)) ;; ---------------------------------- ;; ;; tab-bar ;; ---------------------------------- ;; (editor-feature tab-bar "Provides a frame-wide tab bar that allows for tabbed workspace switching" :custom (tab-bar-format '(tab-bar-format-tabs tab-bar-separator)) (tab-bar-close-button-show nil "Disable close button on tabs")) ;; ---------------------------------- ;; ;; tab-line ;; ---------------------------------- ;; (editor-feature tab-line "Provides a buffer-local tab line that facilitates quick buffer switching" :custom (tab-line-close-button-show nil "Disable close button on tabs") (tab-line-new-button-show nil "Disable tab creation button") (tab-line-left-button nil "Disable the left scroll button") (tab-line-right-button nil "Disable the right scroll button") (tab-line-switch-cycling t "Enable wrap-around tab cycling")) ;; ---------------------------------- ;; ;; uniquify ;; ---------------------------------- ;; (editor-feature uniquify "Provides better unique names when there are name conflicts between buffers" :custom (uniquify-buffer-name-style 'forward "Show file path before buffer name") (uniquify-after-kill-buffer-p t "Update buffer names after killing") (uniquify-ignore-buffers-re "^\\*" "Avoid renaming special buffers")) ;; -------------------------------------------------------------------------- ;; ;; ;; External packages ;; ;; -------------------------------------------------------------------------- ;; (defmacro external-package (name docstring &rest args) "Apply NAME and ARGS to `use-package' with `:ensure' defaulted to t. DOCSTRING is an optional form that is discarded upon expansion." (declare (doc-string 2) (indent defun)) (ignore docstring) `(use-package ,name :ensure t ,@args)) ;; ---------------------------------- ;; ;; Theme setup ;; ---------------------------------- ;; ;; TEMP: Remove :load-path and change to `external-package' once ;; adwaita-dark-theme is available in MELPA (editor-feature adwaita-dark-theme "Provides the `adwaita-dark' theme and custom fringe bitmaps" :load-path "adwaita-dark/" :config (load-theme 'adwaita-dark :no-confirm) (adwaita-dark-theme-arrow-fringe-bmp-enable) (eval-after-load 'diff-hl #'adwaita-dark-theme-diff-hl-fringe-bmp-enable) (eval-after-load 'flymake #'adwaita-dark-theme-flymake-fringe-bmp-enable) (eval-after-load 'flycheck #'adwaita-dark-theme-flycheck-fringe-bmp-enable) (eval-after-load 'neotree #'adwaita-dark-theme-neotree-configuration-enable) :hook (user/after-daemon-make-initial-frame-functions . (lambda () (load-theme 'adwaita-dark :no-confirm)))) ;; ---------------------------------- ;; ;; Simple language modes ;; ---------------------------------- ;; (external-package csharp-mode "Major mode for C#" :mode ("\\.cs\\'" . csharp-mode)) (external-package dart-mode "Major mode for Dart" :mode ("\\.dart\\'" . dart-mode)) (external-package docker-compose-mode "Major mdoe for docker-compose files" :mode ("\\docker-compose.yml\\'" . docker-compose-mode)) (external-package dockerfile-mode "Major mode for Docker files" :mode ("\\Dockerfile\\'" . dockerfile-mode)) (external-package fennel-mode "Major mode for Fennel" :mode ("\\.fnl\\'" . fennel-mode)) (external-package fish-mode "Major mode for fish files" :mode ("\\.fish\\'" . fish-mode)) (external-package gdscript-mode "Major mode for GDScript" :mode ("\\.gd\\'" . gdscript-mode) ("\\.tscn\\'" . gdscript-mode)) (external-package git-modes "Major modes for git files" :mode ("\\.gitignore\\'" . gitignore-mode) ("\\.gitconfig\\'" . gitconfig-mode) ("\\.gitmodules\\'" . gitconfig-mode) ("\\.gitattributes\\'" . gitattributes-mode)) (external-package glsl-mode "Major mode for GLSL" :mode ("\\.frag\\'" . glsl-mode) ("\\.vert\\'" . glsl-mode)) (external-package json-mode "Major mode for JSON files" :mode ("\\.json\\'" . json-mode)) (external-package kotlin-mode "Major mode for Kotlin" :mode ("\\.kt\\'" . kotlin-mode)) (external-package lua-mode "Major mode for Lua" :mode (("\\.lua\\'" . lua-mode) ("\\.rockspec\\'" . lua-mode))) (external-package rust-mode "Major mode for Rust" :mode ("\\.rs\\'" . rust-mode)) (external-package typescript-mode "Major mode for TypeScript" :mode ("\\.tsx?\\'" . typescript-mode)) (external-package web-mode "Major mode for web templates" :custom (web-mode-markup-indent-offset 2 "Use 2 spaces instead of 4 for indenting HTML elements") (web-mode-enable-auto-quoting nil "Do not automatically insert quotes after HTML attributes") :mode (("\\.php\\'" . web-mode) ("\\.html\\'" . web-mode))) ;; ---------------------------------- ;; ;; anzu ;; ---------------------------------- ;; (external-package anzu "Displays matching and replacement information in the mode line" :config (global-anzu-mode) :bind ((" " . anzu-query-replace) (" " . anzu-query-replace-regexp))) ;; ---------------------------------- ;; ;; bufler ;; ---------------------------------- ;; (external-package bufler "Presents open buffers in a grouped and organized menu" :custom (bufler-columns '("Name" "Path")) (bufler-list-group-separators '((0 . "\n"))) (bufler-column-name-modified-buffer-sigil " ●") (bufler-list-display-buffer '((display-buffer-reuse-window display-buffer-same-window))) (bufler-groups (bufler-defgroups (group ;; Subgroup collecting all named workspaces. (auto-workspace)) (group ;; Subgroup collecting all `help-mode' and `info-mode' buffers. (group-or "Help/Info" (mode-match "Help" (rx bos "help-")) (mode-match "Info" (rx bos "info-")))) (group ;; Subgroup collecting all special buffers (i.e. ones that are not file-backed), ;; except certain ones like Dired, Forge, or Magit buffers (which are allowed to ;; fall through to other groups, so they end up grouped with their project buffers). (group-not "Special" (group-or "Special" (mode-match "Magit" (rx bos "magit-")) (mode-match "Forge" (rx bos "forge-")) (mode-match "Dired" (rx bos "dired")) (mode-match "grep" (rx bos "grep-")) (mode-match "Compilation" (rx bos "compilation-")) (auto-file))) (group ;; Subgroup collecting these "special special" buffers ;; separately for convenience. (name-match "Emacs" (rx bos "*" (or "Messages" "Warnings" "scratch" "Backtrace") "*"))) (group ;; Subgroup collecting all other Magit buffers, grouped by directory. (mode-match "Magit" (rx bos "magit-")) (auto-directory)) ;; Remaining special buffers are grouped automatically by mode. (auto-mode)) (group ;; Subgroup collecting buffers in a version-control project, ;; grouping them by directory (using the parent project keeps, ;; e.g. git worktrees with their parent repos). (auto-parent-project) (group-not "Special" ;; This subgroup collects special buffers so they are ;; easily distinguished from file buffers. (group-or "Non-file-backed and neither Dired nor Magit" (mode-match "Magit Status" (rx bos "magit-status")) (mode-match "Dired" (rx bos "dired-")) (auto-file)))) ;; Group remaining buffers by directory, then major mode. (auto-directory) (auto-mode))) :bind (("C-x C-b" . bufler-list))) ;; ---------------------------------- ;; ;; consult ;; ---------------------------------- ;; (external-package consult "Provides a number of autocompletion-enhanced replacements for default commands" :config ;; `eshell-mode-map' is not available until the `esh-mode' module is loaded (eval-after-load 'esh-mode (lambda () (bind-key "C-r" #'consult-history 'eshell-mode-map))) :bind (("M-s l" . consult-line) ("M-s L" . consult-line-multi) ("C-S-s" . consult-line-multi) ("C-x M-b" . consult-buffer-other-window) (" " . consult-goto-line) (" " . consult-line) (" " . consult-buffer) (" " . consult-yank-from-kill-ring) (" " . consult-ripgrep))) ;; ---------------------------------- ;; ;; corfu ;; ---------------------------------- ;; (external-package corfu "Enhances completion suggestions with a visible popup" :init (global-corfu-mode) :config (corfu-popupinfo-mode) :custom (corfu-auto t "Automatically display popups wherever available") (corfu-min-width 20 "Ensure completion popups are at least 20 columns wide") (corfu-max-width 50 "Limit completion popup width to 50 columns") (corfu-popupinfo-hide nil "Don't hide doc popups while scrolling between candidates") (corfu-popupinfo-delay 0.1 "Wait 0.1 seconds before showing a doc popup for a candidate") (corfu-echo-documentation nil "Disable displaying documentation strings in the echo area") :hook ;; Displaying popups aggressively (i.e. without summoning them with a key press) can ;; cause the cursor to jump around in `eshell-mode' (eshell-mode-hook . (lambda () (setq-local corfu-auto nil))) :bind (:map corfu-popupinfo-map ("M-n" . corfu-popupinfo-scroll-up) ("M-p" . corfu-popupinfo-scroll-down))) ;; ---------------------------------- ;; ;; corfu-terminal ;; ---------------------------------- ;; (external-package corfu-terminal "Allows Corfu to function when not running in a graphical frame" :when (not (display-graphic-p)) :config (corfu-terminal-mode)) ;; ---------------------------------- ;; ;; diff-hl ;; ---------------------------------- ;; (external-package diff-hl "Highlights uncommited changes to version controlled files in the gutter" :config (global-diff-hl-mode) :hook (diff-hl-mode-hook . diff-hl-flydiff-mode)) ;; ---------------------------------- ;; ;; fish-completion ;; ---------------------------------- ;; (external-package fish-completion "Sources shell completion candidates from the fish shell" :when (executable-find "fish") :hook (eshell-mode-hook . fish-completion-mode)) ;; ---------------------------------- ;; ;; flycheck ;; ---------------------------------- ;; (external-package flycheck "Enables on-the-fly syntax checking for supported languages" :custom (flycheck-idle-change-delay 2 "Wait for 2 seconds of idling before invoking any checkers") (flycheck-check-syntax-automatically '(save idle-change mode-enabled)) :hook (prog-mode-hook . flycheck-mode) :bind ("C-c e" . flycheck-list-errors)) ;; ---------------------------------- ;; ;; fussy ;; ---------------------------------- ;; (external-package fussy "Provides a flexible `completion-style' that scores and sorts candidates" :demand t :custom (completion-ignore-case t "Ignore case in completion candidates") (completion-category-defaults nil "Disable category-specific completion styles") (completion-category-overrides nil "Disable category-specific completion styles") (completion-styles '(substring fussy basic) "Set fussy as a fallback completion style")) ;; ---------------------------------- ;; ;; hl-todo ;; ---------------------------------- ;; (external-package hl-todo "Highlights certain keywords in comments" :custom (hl-todo-keyword-faces '(("TODO" . hl-todo) ("TEMP" . hl-todo) ("HACK" . hl-todo) ("DEBUG" . hl-todo) ("FIXME" . hl-todo))) :hook (prog-mode-hook . hl-todo-mode)) ;; ---------------------------------- ;; ;; kind-icon ;; ---------------------------------- ;; (external-package kind-icon "Adds contextual icons in front of Corfu completion candidates" :config (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter) :custom (kind-icon-use-icons nil "Use stylized text labels instead of graphical badges") (kind-icon-default-face 'corfu-default "Stylize characters using the same face as Corfu")) ;; ---------------------------------- ;; ;; ligature ;; ---------------------------------- ;; (external-package ligature "Enables support for mapping characters to ligatures" :config (ligature-set-ligatures 'prog-mode '(;; This set of ligatures is for Fira Code, but ;; should work for most any font with ligatures ;; && &&& ;; ;; ;;; ;; %% %%% ;; ?? ??? ?: ?= ?. ;; !! !!! !. !: !!. != !== !~ (";" (rx (+ ";"))) ("&" (rx (+ "&"))) ("%" (rx (+ "%"))) ("?" (rx (or ":" "=" "\." (+ "?")))) ("!" (rx (+ (or "=" "!" "\." ":" "~")))) ;; \\ \\\ \/ ;; ++ +++ ++++ +> ;; :: ::: :::: :> :< := :// ::= ;; // /// //// /\ /* /> /===:===!=//===>>==>==/ ;; == === ==== => =| =>>=>=|=>==>> ==< =/=//=// =~ =:= =!= ("\\" (rx (or "/" (+ "\\")))) ("+" (rx (or ">" (+ "+")))) (":" (rx (or ">" "<" "=" "//" ":=" (+ ":")))) ("/" (rx (+ (or ">" "<" "|" "/" "\\" "\*" ":" "!" "=")))) ("=" (rx (+ (or ">" "<" "|" "/" "~" ":" "!" "=")))) ;; |> ||> |||> ||||> |] |} || ||| |-> ||-|| ;; |->>-||-<<-| |- |== ||=|| |==>>==<<==<=>==//==/=!==:===> ("|" (rx (+ (or ">" "<" "|" "/" ":" "!" "}" "\]" "-" "=" )))) ;; *> */ *) ** *** **** ;; .. ... .... .= .- .? ..= ..< ;; -- --- ---- -~ -> ->> -| -|->-->>->--<<-| ;; #: #= #! #( #? #[ #{ #_ #_( ## ### ##### ;; >: >- >>- >--|-> >>-|-> >= >== >>== >=|=:=>> >> >>> >>>> ("*" (rx (or ">" "/" ")" (+ "*")))) ("\." (rx (or "=" "-" "\?" "\.=" "\.<" (+ "\.")))) ("-" (rx (+ (or ">" "<" "|" "~" "-")))) ("#" (rx (or ":" "=" "!" "(" "\?" "\[" "{" "_(" "_" (+ "#")))) (">" (rx (+ (or ">" "<" "|" "/" ":" "=" "-")))) ;; <>