diff --git a/init.el b/init.el index c641945..808e35c 100644 --- a/init.el +++ b/init.el @@ -1,783 +1,1243 @@ -;;; init.el --- My custom Emacs configuration file. -*- lexical-binding: t; -*- +;;; 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 ;; -;; Jessie Hildebrandt's -;; Run-Anywhere 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 configuration file was designed to work with Emacs 28, but might work -;; with Emacs 27. +;; This file will automatically generate an early-init.el file upon first run, +;; or if early-init.el is missing. ;; -;; Generic keybinds are documented at the top of the "Key Bindings" subsection, -;; and package-specific keybinds are documented above each "use-package" statement -;; in the "EXTERNAL PACKAGES" section. +;; 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: -;;======================================== +;; -------------------------------------------------------------------------- ;; ;; -;; EMACS CONFIGURATION +;; Startup ;; -;;======================================== +;; -------------------------------------------------------------------------- ;; -;;==================== -;; Garbage Collector -;;==================== +(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.") -;; Set the default garbage collection parameters. (~32MB) -(defconst init-file/gc-cons-threshold 32000000 "Preferred garbage collection threshold value.") -(defconst init-file/gc-cons-percentage 0.1 "Preferred garbage collection percentage value.") +;; ---------------------------------- ;; +;; early-init.el file creation +;; ---------------------------------- ;; -;; Define some functions for deferring and restoring Emacs' garbage collection facilities. -(defun defer-garbage-collection () - "Defer garbage collection by maximizing the collection threshold." - (setq gc-cons-threshold most-positive-fixnum - gc-cons-percentage 0.6)) -(defun restore-garbage-collection () - "Restore the garbage collection threshold parameters in a deferred fashion." - (run-at-time - 1 nil (lambda () (setq gc-cons-threshold init-file/gc-cons-threshold - gc-cons-percentage init-file/gc-cons-percentage)))) +(defvar user/early-init-forms + '(";;; early-init.el --- Early initialization -*- lexical-binding: t; -*-" -;; Defer garbage collection while Emacs is starting and restore the threshold to 8MB when we're done. -(defer-garbage-collection) -(add-hook 'emacs-startup-hook #'restore-garbage-collection) + ";; This file is not part of GNU Emacs." -;; Similarly raise and restore the garbage collection threshold for minibuffer commands. -(add-hook 'minibuffer-setup-hook #'defer-garbage-collection) -(add-hook 'minibuffer-exit-hook #'restore-garbage-collection) + ";;; Commentary:" -;; Collect all garbage whenever the focus changes to/from Emacs. -(if (>= emacs-major-version 27) - (add-function :after after-focus-change-function #'garbage-collect) - (with-no-warnings (add-hook 'focus-out-hook #'garbage-collect))) + ";; This file has been automatically generated by init.el." + ";; More relevant commentary is likely in init.el, not here." -;;==================== -;; Variables/Basic Config. -;;==================== + ";;; Code:" -;; Basic variable configuration. -(setq-default - initial-scratch-message "" ; Remove initial message - frame-title-format '("Emacs - %b") ; Window title formatting - truncate-lines t ; Truncate lines instead of wrapping - message-truncate-lines t ; Truncate messages in the echo area - inhibit-startup-screen t ; Don't show startup screen - inhibit-splash-screen t ; Don't show splash screen - cursor-in-non-selected-windows nil ; Don't show cursor in unfocused windows - x-gtk-use-system-tooltips nil ; Don't use system tooltips - mouse-wheel-progressive-speed nil ; Don't accelerate mouse scrolling - scroll-preserve-screen-position 1 ; Don't move cursor while scrolling - scroll-conservatively 101 ; Only scroll one line at a time - scroll-margin 5 ; Maintain a margin of 5 lines while scrolling - max-mini-window-height 10 ; Limit the minibuffer's height to 10 lines - ) + (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%).") -;; Set backup behavior. -(setq-default - backup-by-copying t ; Don't delink hardlinks - version-control t ; Use version numbers on backups - delete-old-versions t ; Do not keep old backups - kept-new-versions 5 ; Keep 5 new versions - kept-old-versions 3 ; Keep 3 old versions - ) + (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)) -;; Configure user directory and file locations. -(defconst custom-backup-dir (concat user-emacs-directory "backups/") "Preferred backup directory.") -(setq-default - custom-file (concat user-emacs-directory "custom.el") ; Use separate custom-vars file - backup-directory-alist `((".*" . ,custom-backup-dir)) ; Set backup file directory - auto-save-file-name-transforms `((".*" ,custom-backup-dir t)) ; Set autosave file directory - ) + ";; Defer garbage collection until after initialization" + (user/defer-garbage-collection) + (add-hook 'emacs-startup-hook #'user/restore-garbage-collection) -;; Enable uniquify for better unique buffer names. -(require 'uniquify) -(setq - uniquify-buffer-name-style 'forward ; Show directory name before buffer name - uniquify-separator "/" ; Use a forward slash separator - uniquify-after-kill-buffer-p t ; Update buffer names after killing - uniquify-ignore-buffers-re "^\\*" ; Ignore special buffers - ) + ";; 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)))) -;; Set the default styling rules to use. -(setq-default - indent-tabs-mode nil ; Don't use tabs for indentation - tab-width 4 ; Use 4 spaces for default by indentation - c-basic-offset 4 ; Same as above, but for C-like languages - c-default-style "bsd" ; Use BSD-style default styling rules for C-like languages - ) + ";; 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"))) -;; (OS-specific) Set the default working directory. -(if (eq system-type 'windows-nt) - (setq default-directory (getenv "USERPROFILE")) - (setq default-directory "~/")) + ";; package.el initialization is handled manually in init.el" + (setq package-enable-at-startup nil) -;; Disable the default terminal bell. -(setq ring-bell-function 'ignore) + ";;; init.el ends here") + "List of forms that are written to the user early-init file.") -;; Disable some unnecessary byte compilation warnings. -(setq byte-compile-warnings '(not - free-vars - unresolved - noruntime - lexical - make-local)) +(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))) -;; Disable some unnecessary native compilation warnings. -(setq native-comp-async-report-warnings-errors nil) +;; 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)) -;; Set default buffer grouping in ibuffer -(setq ibuffer-saved-filter-groups - (quote (("default" - ("Emacs" (or - (name . "^\\*.*\\*.*$") - (name . "^magit.*:.*$"))))))) -(add-hook 'ibuffer-mode-hook (lambda () (ibuffer-switch-to-saved-filter-groups "default"))) +;; 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))) -;; Temporarily disable file handler checking during startup to save time. -(defvar temp--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 temp--file-name-handler-alist))) +;; ---------------------------------- ;; +;; Package manager initialization +;; ---------------------------------- ;; -;; Add a hook to delete any trailing whitespace before saving a file. -(add-hook 'before-save-hook 'delete-trailing-whitespace) - -;;==================== -;; Misc. Mode Config -;;==================== - -;; Enable Modes -(mapc (lambda (mode) (funcall mode 1)) - '( - global-subword-mode ; Treats camel-case names as multiple words - global-auto-revert-mode ; Automatically revert buffers on file changes - global-hl-line-mode ; Highlight the currently-selected line - column-number-mode ; Show column number in the mode line - show-paren-mode ; Highlight matching parenthesis - size-indication-mode ; Show buffer size in the mode line - savehist-mode ; Persist minibuffer history across sessions - )) - -;; Disable Modes -(mapc (lambda (mode) (funcall mode 0)) - `( - menu-bar-mode ; Disable the menu bar - )) - -;; Hooked Modes -(add-hook 'prog-mode-hook (lambda () - (unless (derived-mode-p 'lisp-interaction-mode) - (display-line-numbers-mode)))) ; Display line numbers -(add-hook 'prog-mode-hook 'electric-pair-local-mode) ; Automatic delimiter pairing - -;; Mode Configuration -(setq - show-paren-delay 0.0 ; (show-paren-mode) - Parenthesis highlighting delay - visual-line-fringe-indicators t ; (visual-line-mode) - Shows fringe indicators for wrapping - tab-line-close-button-show nil ; (tab-line-mode) - Do not show close button on tabs - tab-line-new-button-show nil ; (tab-line-mode) - Do not show "new tab" button - ) - -;;==================== -;; New Frame Config -;;==================== - -;; Define a function that will configure new Emacs frames. -(defvar init-file/theme-loaded nil "Whether or not the theme has been loaded yet.") -(defun configure-new-frame (frame) - "Configure new frame FRAME." - (when (display-graphic-p frame) - (select-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))) - (tool-bar-mode 0) - (scroll-bar-mode 0) - (when (and - (daemonp) - (not init-file/theme-loaded)) - (mood-one-theme-arrow-fringe-bmp-enable) - (load-theme 'mood-one t) - (solaire-global-mode t) - (set-fontset-font t 'symbol "Symbola" nil 'append) - (setq init-file/theme-loaded t)))) - -;; Define a function that will inhibit startup messages for new Emacs frames. -(defun inhibit-new-frame-message () - "Inhibit startup messages in new frames." - (setq inhibit-message t) - (run-with-idle-timer 0 nil (lambda () (setq inhibit-message nil)))) - -;; Hook the frame configuration function into all newly-created frames. -(add-hook 'after-make-frame-functions #'configure-new-frame) - -;; Hook the startup message inhibition function into all newly-created frames. -(add-hook 'server-after-make-frame-hook #'inhibit-new-frame-message) - -;; Run for any already-existing frames -(mapc 'configure-new-frame (frame-list)) - -;;==================== -;; Package Manager -;;==================== - -;; Temporary fix for bugged package archive retrieval in Emacs 26.2. -(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3") - -;; Configure the package manager. +;; To be evaluated at both compile time and run time (eval-and-compile - - ;; Configure the package manager and use-package settings. - (setq load-prefer-newer t - package-user-dir (concat user-emacs-directory "elpa") + (setq package-user-dir (locate-user-emacs-file "package/") + package-native-compile t package-check-signature nil - package-enable-at-startup nil - package--init-file-ensured t - use-package-always-ensure t - use-package-always-defer t) + use-package-hook-name-suffix nil + use-package-always-demand (daemonp))) - ;; Make sure that the package directory exists to prevent errors. - (unless (file-directory-p package-user-dir) - (make-directory package-user-dir t)) - - ;; Manually assemble the load-path during startup to save time. - (setq load-path (append load-path (directory-files package-user-dir t "^[^.]" t)))) - -;; Prepare a stupid hack to fix an issue with use-package in byte-compiled code on some systems. (Emacs 27+) -(deftheme use-package) -(enable-theme 'use-package) -(setq custom-enabled-themes (remq 'use-package custom-enabled-themes)) - -;; Initialize the package management system (only at compile time). +;; To be evaluated only at compile time (eval-when-compile - ;; Require the package manager. + ;; Initialize package.el (require 'package) - - ;; Enable the MELPA repository. - (unless (assoc-default "melpa" package-archives) - (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)) - - ;; Initialize the package manager. - (package-initialize) + (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) + (unless (bound-and-true-p package--initialized) + (package-initialize)) (package-refresh-contents) - ;; Check for use-package. Install it if not already present. + ;; Install use-package (unless (package-installed-p 'use-package) (package-install 'use-package)) - (require 'use-package)) + (require 'use-package) -;;==================== -;; Key Bindings -;;==================== + ;; 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))))) -;; Custom Bindings: -;; [ F6 ] -> Toggle line-wrapping -;; [ F7 ] -> Toggle display-line-numbers-mode -;; [ F10 ] -> (Overwritten) Open the menubar in a minibuffer -;; [ C-z ] -> (Overwritten) Undo (and don't suspend, thanks) -;; [ C-c d ] -> Delete pair -;; [ C-c o ] -> Focus on minibuffer window -;; [ C-c r ] -> Revert buffer without confirmation -;; [ C-c s ] -> Open scratch buffer in the current window -;; [ C-c RET ] -> Open terminal in the current window -;; [ C-x C-b ] -> (Overwritten) Invoke ibuffer -;; [ M-n / M-p ] -> Scroll up/down by one line -;; [ C-M-n / C-M-p ] -> Move forward/back by one paragraph -;; [ C-c C- ] -> Focus on the window in +;; 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)) -;; Require bind-key. (Bundled with use-package) -(require 'bind-key) +;; -------------------------------------------------------------------------- ;; +;; +;; Custom interactive functions +;; +;; -------------------------------------------------------------------------- ;; -(defun switch-to-minibuffer-window () - "Switch to the minibuffer window (if active)." +(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." + (interactive) + (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 revert-buffer-no-confirm () - "Revert the current buffer without confirmation." - (interactive) - (if (not (buffer-modified-p)) - (revert-buffer :ignore-auto :noconfirm) - (when (yes-or-no-p "The contents of this buffer have been modified. Really revert? ") - (revert-buffer :ignore-auto :noconfirm)))) - -(defun open-scratch-buffer () +(defun user/scratch-buffer () "Open the scratch buffer, (re)creating it if not present." (interactive) - (if (get-buffer "*scratch*") - (switch-to-buffer "*scratch*") - (progn - (switch-to-buffer (get-buffer-create "*scratch*")) - (lisp-interaction-mode)))) + (pop-to-buffer (get-buffer-create "*scratch*") '((display-buffer-reuse-window display-buffer-same-window))) + (lisp-interaction-mode)) -(defun open-default-shell () - "Open the default shell in `ansi-term', switching to an open session if present." - (interactive) - (if (get-buffer "*terminal*") - (switch-to-buffer "*terminal*") - (progn - (ansi-term shell-file-name) - (rename-buffer "*terminal*")))) +(defun user/scan-directory-for-projects () + "Prompt for a directory and then scan for any project roots within. -;; Set up all custom bindings (non-package-specific) -(bind-keys ([f6] . visual-line-mode) - ([f7] . display-line-numbers-mode) - ([f10] . tmm-menubar) - ("C-z" . undo) - ("C-c d" . delete-pair) - ("C-c o" . switch-to-minibuffer-window) - ("C-c r" . revert-buffer-no-confirm) - ("C-c s" . open-scratch-buffer) - ("C-c RET" . open-default-shell) - ("C-x C-b" . ibuffer) - ("M-n" . scroll-up-line) - ("M-p" . scroll-down-line) - ("C-M-n" . forward-paragraph) - ("C-M-p" . backward-paragraph)) -(bind-keys* ("C-c C-i" . windmove-up) - ("C-c C-k" . windmove-down) - ("C-c C-j" . windmove-left) - ("C-c C-l" . windmove-right)) - -;;==================== -;; Init File -;;==================== - -(defun init-file/open-init-file () - "Opens the init file in a buffer." - (interactive) - (find-file user-init-file)) - -(defun init-file/download-latest-init-file () - "Download the latest init file from jessieh.net." - (interactive) - (when (yes-or-no-p "Download latest init file from jessieh.net? ") - (message "Updating init file...") - (url-copy-file "https://jessieh.net/emacs" (concat user-emacs-directory "init.el") t) - (init-file/byte-compile-init-file))) - -(defun init-file/byte-compile-init-file () - "Byte compile the init file." - (interactive) - (save-restriction - (message "Byte-compiling init file...") - (byte-compile-file (concat user-emacs-directory "init.el")))) - -(defun init-file/refresh-packages () - "Refresh all packages that have been configured for use in the init file." - (interactive) - (when (yes-or-no-p "Redownload and refresh packages? ") - (message "Refreshing packages...") - (delete-directory package-user-dir t) - (init-file/byte-compile-init-file))) - -(defun init-file/project-find-projects () - "Prompt for a directory and then search 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-list (mapcar 'file-name-directory (directory-files-recursively directory "\\.git$" 'valid-root-p t ))) - (num-projects-found (length project-list)) - (temp-buffer-name (concat "*" (number-to-string num-projects-found) " Project Roots Found*")) - (temp-buffer (with-output-to-temp-buffer temp-buffer-name (progn (princ (mapconcat 'identity project-list "\n")) - standard-output)))) + (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? ") - (mapc 'project-remember-projects-under project-list) + (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.")) - (kill-buffer temp-buffer))) + (with-current-buffer temp-buffer-name + (kill-buffer-and-window)))) -;; Make sure that this init file is byte-compiled whenever it changes. -(if (file-newer-than-file-p - (concat user-emacs-directory "init.el") - (concat user-emacs-directory "init.elc")) - (add-hook 'emacs-startup-hook 'init-file/byte-compile-init-file)) - -;;======================================== +;; -------------------------------------------------------------------------- ;; ;; -;; THEME CONFIGURATION +;; Internal/built-in packages ;; -;;======================================== +;; -------------------------------------------------------------------------- ;; -;;==================== -;; Font Configuration -;;==================== +(defmacro editor-feature (name docstring &rest args) + "Apply NAME and ARGS to `use-package' with `:ensure' defaulted to nil. -;; Set up styling for the default face. -(set-face-attribute 'default nil - :family "Fira Code Regular" - :height 100) +DOCSTRING is an optional form that is discarded upon expansion." + (declare (doc-string 2) + (indent defun)) + (ignore docstring) + `(use-package ,name :ensure nil ,@args)) -;; Ensure that the monocolor "Symbola" font can be used for emoji and unicode symbols. -;; (Emacs 27+ on some systems may not have a monocolor fallback font set after multicolor emoji fonts.) -(set-fontset-font t 'symbol "Symbola" nil 'append) +;; ---------------------------------- ;; +;; General editor setup +;; ---------------------------------- ;; -;;==================== -;; Theme -;;==================== +(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" -;; Load "mood-one" -(use-package mood-one-theme - :after - (solaire-mode) - :demand - t :config - (mood-one-theme-arrow-fringe-bmp-enable) - (setq diff-hl-fringe-bmp-function #'mood-one-theme-diff-hl-fringe-bmp-function) - (eval-after-load 'flycheck #'mood-one-theme-flycheck-fringe-bmp-enable) - (eval-after-load 'flymake #'mood-one-theme-flymake-fringe-bmp-enable) - (eval-after-load 'neotree #'mood-one-theme-neotree-configuration-enable) - (load-theme 'mood-one t)) -;;==================== -;; Mode-Line -;;==================== + ;; 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) -;; Load "mood-line" -(use-package mood-line - :demand - t - :config - (mood-line-mode)) - -;;==================== -;; Solaire-Mode -;;==================== - -;; Load "solaire-mode" -(use-package solaire-mode - :demand - t :custom - (solaire-mode-remap-modeline nil) - (solaire-mode-real-buffer-fn - (lambda () - (and (not (string-equal major-mode "neotree-mode")) - (buffer-name (buffer-base-buffer)) - (not (string-match "\*Echo Area" (buffer-name (buffer-base-buffer))))))) - :config - (solaire-global-mode t)) -;;======================================== -;; -;; EXTERNAL PACKAGE CONFIGURATION -;; -;;======================================== + ;; Default working directory + (default-directory (if (eq system-type 'windows-nt) + (setq default-directory (getenv "USERPROFILE")) + (setq default-directory "~/"))) -;;==================== -;; Language Modes -;;==================== + ;; 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") -;; Currently Supported: -;; Lua, Fennel, PHP, Rust, Fish, C#, Dart, Kotlin, GDScript, GLSL + ;; 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") -;; Misc. Supported: -;; Docker, docker-compose + ;; Default style rules + (indent-tabs-mode nil "Use spaces for indentation") + (tab-width 4 "Use 4 spaces for indentation") -;; Load Lua Mode -;; (Associated files: .lua, .rockspec) -(use-package lua-mode - :mode - (("\\.lua\\'" . lua-mode) - ("\\.rockspec\\'" . lua-mode))) + ;; 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") -;; Load Fennel Mode -;; (Associated files: .fnl) -(use-package fennel-mode - :mode - ("\\.fnl\\'" . fennel-mode)) + ;; 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" -;; Load Web Mode -;; (Associated files: .php, .html) -;; (In js-jsx-mode: .jsx) -(use-package web-mode :custom - (web-mode-markup-indent-offset 2) - (web-mode-enable-auto-quoting nil) - :mode - (("\\.php\\'" . web-mode) - ("\\.html\\'" . web-mode) - ("\\.jsx\\'" . js-jsx-mode))) -;; Load TypeScript mode -;; (Associated files: .ts, .tsx) -(use-package typescript-mode - :mode - (("\\.tsx?\\'" . typescript-mode))) + ;; Config file + (custom-file user/custom-file "Store customization info in a separate file") -;; Load JSON Mode -;; (Associated files: .json) -(use-package json-mode - :mode - ("\\.json\\'" . json-mode)) + ;; 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))) -;; Load Rust Mode -;; (Associated files: .rs) -(use-package rust-mode - :mode - ("\\.rs\\'" . rust-mode)) + ;; 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") -;; Load Fish Mode -;; (Associated files: .fish) -(use-package fish-mode - :mode - ("\\.fish\\'" . fish-mode)) + :hook + (before-save-hook . delete-trailing-whitespace)) -;; Load C Sharp Mode -;; (Associated files: .cs) -(use-package csharp-mode +;; ---------------------------------- ;; +;; 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" + :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)) -;; Load Dart Mode -;; (Associated files: .dart) -(use-package dart-mode +(external-package dart-mode + "Major mode for Dart" :mode ("\\.dart\\'" . dart-mode)) -;; Load Kotlin Mode -;; (Associated files: .kt) -(use-package kotlin-mode +(external-package docker-compose-mode + "Major mdoe for docker-compose files" :mode - ("\\.kt\\'" . kotlin-mode)) + ("\\docker-compose.yml\\'" . docker-compose-mode)) -;; Load GDScript Mode -;; (Associated files: .gd, .tscn) -(use-package gdscript-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)) -;; Load GLSL Mode -;; (Associated files: .frag, .vert) -(use-package glsl-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)) -;;==================== -;; File-Specific Modes -;;==================== - -;; Load Dockerfile Mode -(use-package dockerfile-mode +(external-package json-mode + "Major mode for JSON files" :mode - ("\\Dockerfile\\'" . dockerfile-mode)) + ("\\.json\\'" . json-mode)) -;; Load Docker Compose Mode -(use-package docker-compose-mode +(external-package kotlin-mode + "Major mode for Kotlin" :mode - ("\\docker-compose.yml\\'")) + ("\\.kt\\'" . kotlin-mode)) -;; Load [gitignore/gitattributes/gitconfig] Mode -(use-package git-modes +(external-package lua-mode + "Major mode for Lua" :mode - ("\\.gitignore\\'" . gitignore-mode) - ("\\.gitattributes\\'" . gitattributes-mode)) + (("\\.lua\\'" . lua-mode) + ("\\.rockspec\\'" . lua-mode))) -;;==================== -;; LSP Mode (Language Server Support) -;;==================== +(external-package rust-mode + "Major mode for Rust" + :mode + ("\\.rs\\'" . rust-mode)) -;; Load LSP Mode -;; Associated languages: HTML, JavaScript, TypeScript, CSS, JSON, GDScript, Rust -(use-package lsp-mode - :hook - (web-mode . lsp-deferred) - (js-mode . lsp-deferred) - (typescript-mode . lsp-deferred) - (css-mode . lsp-deferred) - (json-mode . lsp-deferred) - (gdscript-mode . lsp-deferred) - (rust-mode . lsp-deferred) +(external-package typescript-mode + "Major mode for TypeScript" + :mode + ("\\.tsx?\\'" . typescript-mode)) + +(external-package web-mode + "Major mode for web templates" :custom - (lsp-signature-auto-activate nil) - (lsp-signature-render-documentation nil) - (lsp-eldoc-hook nil) - (lsp-headerline-breadcrumb-enable nil) - (lsp-modeline-code-actions-enable nil) - :commands - (lsp - lsp-deferred)) + (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))) -;; Load LSP UI -(use-package lsp-ui - :custom - (lsp-ui-imenu-enable nil) - (lsp-ui-peek-enable t) - (lsp-ui-peek-always-show t) - (lsp-ui-doc-enable nil) - (lsp-ui-doc-max-width 50) - (lsp-ui-doc-max-height 10) - (lsp-ui-doc-position 'top) - (lsp-ui-doc-alignment 'window) - (lsp-ui-doc-header t) - (lsp-ui-doc-include-signature t) - (lsp-ui-doc-show-with-cursor t) +;; ---------------------------------- ;; +;; anzu +;; ---------------------------------- ;; + +(external-package anzu + "Displays matching and replacement information in the mode line" :config - (setq lsp-ui-doc-border (face-background 'lsp-ui-doc-background)) + (global-anzu-mode) :bind - (:map lsp-ui-mode-map - ("M-," . lsp-ui-doc-mode) - ("M-." . lsp-ui-peek-find-definitions) - ("M-?" . lsp-ui-peek-find-references))) + ((" " . anzu-query-replace) + (" " . anzu-query-replace-regexp))) -;;==================== -;; SLIME (Lisp Interaction Framework) -;;==================== +;; ---------------------------------- ;; +;; bufler +;; ---------------------------------- ;; -;; Load SLIME -(use-package slime - :config - (setq inferior-lisp-program (executable-find "sbcl")) - :commands - (slime)) - -;; Load SLIME-docker -(use-package slime-docker +(external-package bufler + "Presents open buffers in a grouped and organized menu" :custom - (slime-docker-program "sbcl") - :commands - (slime-docker)) + (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))) -;;==================== -;; Package-Lint (Elisp Package Linter) -;;==================== +;; ---------------------------------- ;; +;; consult +;; ---------------------------------- ;; -;; Load Package-Lint -(use-package package-lint - :commands - (package-lint-current-buffer)) +(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))) -;;==================== -;; FlyCheck (Syntax Checker) -;;==================== +;; ---------------------------------- ;; +;; corfu +;; ---------------------------------- ;; -;; Bindings -;; [ C-c e ] -> Open error list window +(external-package corfu + "Enhances completion suggestions with a visible popup" + :config + (global-corfu-mode) + (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-echo-documentation nil "Disable displaying documentation strings in the echo area")) -;; Load FlyCheck -(use-package flycheck +;; ---------------------------------- ;; +;; 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 - (prog-mode . flycheck-mode) - (c++-mode-hook . (lambda () (setq flycheck-clang-standard "c++17"))) - :config - (advice-add 'flycheck-eslint-config-exists-p :override (lambda() t)) + (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-python-flake8-executable "flake8") - (flycheck-python-pylint-executable "pylint") - (flycheck-python-mypy-executable "mypy") + (flycheck-idle-change-delay 2 "Wait for 2 seconds of idling before invoking any checkers") (flycheck-check-syntax-automatically '(save idle-change mode-enabled)) - (flycheck-idle-change-delay 2) + :hook + (prog-mode-hook . flycheck-mode) :bind ("C-c e" . flycheck-list-errors)) -;; [Elisp Packages] -;; Load FlyCheck-Package -(use-package flycheck-package - :after - (flycheck) - :hook - (flycheck-mode-hook . flycheck-package-setup)) +;; ---------------------------------- ;; +;; fussy +;; ---------------------------------- ;; -;;==================== -;; Corfu (Autocompletion) -;;==================== - -;; Load Corfu -(use-package corfu +(external-package fussy + "Provides a flexible `completion-style' that scores and sorts candidates" :demand t :custom - (corfu-auto t) - (corfu-echo-documentation nil) - (corfu-min-width 20) - (corfu-max-width 50) - :config - (global-corfu-mode t)) + (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 '(basic substring fussy) "Set fussy as a fallback completion style")) -;; Load Corfu-doc -(use-package corfu-doc - :demand - t - :after - (corfu) - :config - (corfu-doc-mode t) - :bind - (:map corfu-map - ("M-p" . corfu-doc-scroll-down) - ("M-n" . corfu-doc-scroll-up) - ("M-d" . corfu-doc-toggle))) +;; ---------------------------------- ;; +;; hl-todo +;; ---------------------------------- ;; -;; Load Corfu-terminal -(use-package corfu-terminal - :demand - t - :after - (corfu) - :config - (unless (display-graphic-p) - (corfu-terminal-mode t))) +(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)) -;; Load kind-icon (Icons for Corfu completion symbols) -(use-package kind-icon - :demand - t - :after - (corfu) +;; ---------------------------------- ;; +;; 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-default-face 'corfu-default) - (kind-icon-use-icons nil)) + (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")) -;;==================== -;; yasnippet (Snippet Insertion/Completion) -;;==================== +;; ---------------------------------- ;; +;; ligature +;; ---------------------------------- ;; -;; Load yasnippet -(use-package yasnippet - :demand - t - :custom - (yas-also-auto-indent-first-line t) - (yas-also-indent-empty-lines t) +(external-package ligature + "Enables support for mapping characters to ligatures" :config - (yas-global-mode t)) + (ligature-set-ligatures 'prog-mode + '(;; This set of ligatures is for Fira Code, but + ;; should work for most any font with ligatures -;;==================== -;; restclient (REST Interaction Mode) -;;==================== + ;; && &&& + ;; ;; ;;; + ;; %% %%% + ;; ?? ??? ?: ?= ?. + ;; !! !!! !. !: !!. != !== !~ + (";" (rx (+ ";"))) + ("&" (rx (+ "&"))) + ("%" (rx (+ "%"))) + ("?" (rx (or ":" "=" "\." (+ "?")))) + ("!" (rx (+ (or "=" "!" "\." ":" "~")))) -;; Load restclient -(use-package restclient - :init - (defun restclient () - "Opens the restclient buffer, (re)creating it if not present." - (interactive) - (if (get-buffer "*restclient*") - (switch-to-buffer "*restclient*") - (progn - (switch-to-buffer (get-buffer-create "*restclient*")) - (restclient-mode)))) + ;; \\ \\\ \/ + ;; ++ +++ ++++ +> + ;; :: ::: :::: :> :< := :// ::= + ;; // /// //// /\ /* /> /===:===!=//===>>==>==/ + ;; == === ==== => =| =>>=>=|=>==>> ==< =/=//=// =~ =:= =!= + ("\\" (rx (or "/" (+ "\\")))) + ("+" (rx (or ">" (+ "+")))) + (":" (rx (or ">" "<" "=" "//" ":=" (+ ":")))) + ("/" (rx (+ (or ">" "<" "|" "/" "\\" "\*" ":" "!" "=")))) + ("=" (rx (+ (or ">" "<" "|" "/" "~" ":" "!" "=")))) + + ;; |> ||> |||> ||||> |] |} || ||| |-> ||-|| + ;; |->>-||-<<-| |- |== ||=|| |==>>==<<==<=>==//==/=!==:===> + ("|" (rx (+ (or ">" "<" "|" "/" ":" "!" "}" "\]" "-" "=" )))) + + ;; *> */ *) ** *** **** + ;; .. ... .... .= .- .? ..= ..< + ;; -- --- ---- -~ -> ->> -| -|->-->>->--<<-| + ;; #: #= #! #( #? #[ #{ #_ #_( ## ### ##### + ;; >: >- >>- >--|-> >>-|-> >= >== >>== >=|=:=>> >> >>> >>>> + ("*" (rx (or ">" "/" ")" (+ "*")))) + ("\." (rx (or "=" "-" "\?" "\.=" "\.<" (+ "\.")))) + ("-" (rx (+ (or ">" "<" "|" "~" "-")))) + ("#" (rx (or ":" "=" "!" "(" "\?" "\[" "{" "_(" "_" (+ "#")))) + (">" (rx (+ (or ">" "<" "|" "/" ":" "=" "-")))) + + ;; <>