;; init.el --- GNU Emacs Initialization File -*- lexical-binding: t; -*- ;; Copyright (c) 2023 Andrew Scott ;; Author: Andrew Scott ;; Keywords: convenience, tools ;; URL: https://codeberg.org/andyscott/dotfiles ;; This file is not part of GNU Emacs. ;;; Commentary: ;; My personal Emacs configuration. ;;; License: ;; MIT No Attribution ;; Permission is hereby granted, free of charge, to any person obtaining a copy of this ;; software and associated documentation files (the "Software"), to deal in the Software ;; without restriction, including without limitation the rights to use, copy, modify, ;; merge, publish, distribute, sublicense, and/or sell copies of the Software, and to ;; permit persons to whom the Software is furnished to do so. ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, ;; INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A ;; PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT ;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION ;; OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ;; SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ;;; Code: ;; Profile emacs startup (add-hook 'emacs-startup-hook (lambda () (message "Emacs loaded in %s with %d garbage collections." (format "%.2f seconds" (float-time (time-subtract (current-time) before-init-time))) gcs-done))) ;;; Initialize elpaca (defvar elpaca-installer-version 0.6) (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory)) (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git" :ref nil :files (:defaults (:exclude "extensions")) :build (:not elpaca--activate-package))) (let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory)) (build (expand-file-name "elpaca/" elpaca-builds-directory)) (order (cdr elpaca-order)) (default-directory repo)) (add-to-list 'load-path (if (file-exists-p build) build repo)) (unless (file-exists-p repo) (make-directory repo t) (when (< emacs-major-version 28) (require 'subr-x)) (condition-case-unless-debug err (if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*")) ((zerop (call-process "git" nil buffer t "clone" (plist-get order :repo) repo))) ((zerop (call-process "git" nil buffer t "checkout" (or (plist-get order :ref) "--")))) (emacs (concat invocation-directory invocation-name)) ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch" "--eval" "(byte-recompile-directory \".\" 0 'force)"))) ((require 'elpaca)) ((elpaca-generate-autoloads "elpaca" repo))) (progn (message "%s" (buffer-string)) (kill-buffer buffer)) (error "%s" (with-current-buffer buffer (buffer-string)))) ((error) (warn "%s" err) (delete-directory repo 'recursive)))) (unless (require 'elpaca-autoloads nil t) (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (load "./elpaca-autoloads"))) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order)) ;; use-package (elpaca elpaca-use-package (elpaca-use-package-mode) (setq elpaca-use-package-by-default t)) (elpaca-wait) ;;; Built-in features (use-package emacs :elpaca nil :init ;;; Vertico ;; Add prompt to `completing-read-multiple' (defun crm-indicator (args) (cons (format "[CRM%s] %s" (replace-regexp-in-string "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" "" crm-separator) (car args)) (cdr args))) (advice-add #'completing-read-multiple :filter-args #'crm-indicator) ;; Recursive minibuffers (setq enable-recursive-minibuffers t) ;; Disable prompt in minibuffer (setq minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt)) (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode) :bind (:map global-map ;; Disable suspend-frame shortcut ("C-z" . nil) ;; Escape behaves like C-g ("" . keyboard-escape-quit) ;; Resizing buffers ("S-C-" . shrink-window-horizontally) ("S-C-" . enlarge-window-horizontally) ("S-C-" . shrink-window) ("S-C-" . enlarge-window)) :config ;;; General Options ;; Better scrolling (setq scroll-step 1 scroll-conservatively 1000 scroll-preserve-screen-position t) (when (fboundp 'pixel-scroll-precision-mode) (pixel-scroll-precision-mode t)) ;; Cursor (setq-default cursor-type 'bar blink-cursor-delay 1.0) ;; Tabs (setq tab-always-indent 'complete) (setq-default tab-width 4) ;; Prefer UTF-8 character coding (prefer-coding-system 'utf-8) (set-default-coding-systems 'utf-8) (set-terminal-coding-system 'utf-8) (set-keyboard-coding-system 'utf-8)) (use-package compile :elpaca nil :commands (compile recompile) :config (setq compilation-scroll-output t)) (use-package cus-edit :elpaca nil :config (setq-default custom-file null-device)) (use-package display-fill-column-indicator :elpaca nil :hook ((conf-mode markdown-mode prog-mode) . display-fill-column-indicator-mode) :config (setq-default fill-column 100)) (use-package display-line-numbers :elpaca nil :hook ((conf-mode prog-mode) . display-line-numbers-mode)) (use-package elec-pair :elpaca nil :defer 3 :config (electric-pair-mode)) (use-package files :elpaca nil :config (let ((auto_save_directory (concat xdg_cache_home "auto-save-list/"))) (setq backup-directory-alist `((".*" . ,auto_save_directory)) auto-save-file-name-transforms `((".*" ,auto_save_directory t)))) (setq backup-by-copying t confirm-kill-processes nil delete-old-versions t kept-new-versions 5 kept-old-versions 3 require-final-newline t version-control t)) (use-package flyspell :elpaca nil :defer 3 :hook (((git-commit-mode markdown-mode) . flyspell-mode) (lsp-mode . flyspell-prog-mode))) (use-package mwheel :elpaca nil :config (setq mouse-wheel-follow-mouse t mouse-wheel-progressive-speed nil mouse-wheel-scroll-amount '(2 ((shift) . 1)))) (use-package paren :elpaca nil :defer 3 :config (show-paren-mode)) (use-package recentf :elpaca nil :config (setq recentf-auto-cleanup 'never recentf-max-menu-items 15 recentf-max-saved-items 100 recentf-save-file (concat xdg_cache_home "recentf")) (recentf-mode)) (use-package savehist :elpaca nil :defer 3 :config (setq savehist-autosave-interval 60 savehist-additional-variables '(search-ring regexp-search-ring) savehist-file (concat xdg_cache_home "history")) (savehist-mode)) (use-package saveplace :elpaca nil :defer 1 :config (setq save-place-file (concat xdg_cache_home "places")) (save-place-mode)) (use-package simple :elpaca nil :config (setq-default indent-tabs-mode nil) (column-number-mode)) (use-package whitespace :elpaca nil :hook ((conf-mode prog-mode text-mode) . whitespace-mode) :config (add-hook 'before-save-hook #'whitespace-cleanup) (setq whitespace-line-column nil whitespace-style '(face lines-char missing-newline-at-eof space-after-tab space-before-tab tabs trailing))) ;;; Packages ;; Benchmark Emacs startup (use-package esup :defer t :custom (esup-depth 0)) ;; Theme (use-package everforest :elpaca (everforest :host sourcehut :repo "theorytoe/everforest-theme") :config (setq custom-safe-themes t) (load-theme 'everforest-hard-dark t) (let ((moody_line (face-attribute 'mode-line :foreground)) (moody_inactive (face-attribute 'mode-line-inactive :background))) (set-face-attribute 'mode-line nil :overline moody_line) (set-face-attribute 'mode-line-inactive nil :overline moody_line) (set-face-attribute 'mode-line-inactive nil :underline moody_line) (set-face-attribute 'mode-line nil :box nil) (set-face-attribute 'mode-line-inactive nil :box nil) (set-face-attribute 'mode-line-inactive nil :background moody_inactive)) :custom-face (cursor ((t (:background "#7fbbb3")))) (whitespace-big-indent ((t (:background "#9da9a0")))) (whitespace-empty ((t (:background "#9da9a0")))) (whitespace-hspace ((t (:background "#9da9a0")))) (whitespace-indentation ((t (:background "#9da9a0")))) (whitespace-line ((t (:background "#e67e80")))) (whitespace-newline ((t (:background "#e67e80")))) (whitespace-space ((t (:background "#9da9a0")))) (whitespace-space-after-tab ((t (:background "#9da9a0")))) (whitespace-space-before-tab ((t (:background "#9da9a0")))) (whitespace-tab ((t (:background "#9da9a0")))) (whitespace-trailing ((t (:background "#e67e80"))))) (elpaca-wait) ;; Mode line (use-package moody :defer 1 :config (setq x-underline-at-descent-line t) (moody-replace-eldoc-minibuffer-message-function) (moody-replace-mode-line-buffer-identification) (moody-replace-mode-line-front-space) (moody-replace-vc-mode)) (use-package minions :after (moody) :config (minions-mode)) (use-package keycast :defer t :config (keycast-mode-line-insert-after 'moody-mode-line-buffer-identification)) ;; Show keybind hints (use-package which-key :defer 3 :config (which-key-mode) :custom (which-key-idle-delay 0.3)) ;; Flyspell corrections (use-package flyspell-correct :after (flyspell) :bind (:map flyspell-mode-map ("C-M-." . flyspell-correct-wrapper))) ;; Better annotations (use-package marginalia :defer 3 :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :config (marginalia-mode)) ;; Better undo/redo (use-package undo-tree :defer 3 :config (global-undo-tree-mode) :custom (undo-tree-auto-save-history t) (undo-tree-history-directory-alist `((".*" . ,(concat xdg_cache_home "undo-tree/"))))) ;; Completion style (use-package orderless :defer 1 :custom (completion-styles '(orderless basic)) (completion-category-overrides '((file (styles basic partial-completion))))) ;; Completion UI (use-package vertico :init (vertico-mode) :custom (vertico-cycle t) (vertico-count 10) (vertico-resize t) (vertico-scroll-margin 1) (completion-ignore-case t) (read-buffer-completion-ignore-case t) (read-file-name-completion-ignore-case t)) ;; Command menu for items around point (use-package embark :defer 3 :init (setq prefix-help-command #'embark-prefix-help-command) ;; documentation from first source (add-hook 'eldoc-documentation-functions #'embark-eldoc-first-target) ;; documentation from multiple sources ;; (setq eldoc-documentation-strategy #'eldoc-documentation-compose-eagerly) :bind (("C-." . embark-act) ("C-;" . embark-dwim) ("C-h B" . embark-bindings)) :config ;; Hide the Embark live/completions buffer mode line (push '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*" nil (window-parameters (mode-line-format . none))) display-buffer-alist)) ;; Search and navigation (use-package consult :init ;; Register formatting (setq register-preview-delay 0.6 register-preview-function #'consult-register-format) ;; Register preview window (advice-add #'register-preview :override #'consult-register-window) ;; Select xref locations with preview (setq xref-show-xrefs-function #'consult-xref xref-show-definitions-function #'consult-xref) :custom (consult-narrow-key "<") (consult-preview-key 'any) ;; Switch project root function ;; (autoload 'projectile-project-root "projectile") :bind (;; C-c bindings in `mode-specific-map' ("C-c M-x" . consult-mode-command) ("C-c h" . consult-history) ("C-c k" . consult-kmacro) ("C-c m" . consult-man) ("C-c i" . consult-info) ([remap Info-search] . consult-info) ;; C-x bindings in `ctl-x-map' ("C-x M-:" . consult-complex-command) ; orig. repeat-complex-command ("C-x b" . consult-buffer) ; orig. switch-to-buffer ("C-x 4 b" . consult-buffer-other-window) ; orig. switch-to-buffer-other-window ("C-x 5 b" . consult-buffer-other-frame) ; orig. switch-to-buffer-other-frame ("C-x r b" . consult-bookmark) ; orig. bookmark-jump ("C-x p b" . consult-project-buffer) ; orig. project-switch-to-buffer ("C-x M-r" . consult-recent-file) ;; Custom M-# bindings for fast register access ("M-#" . consult-register-load) ("M-'" . consult-register-store) ; orig. abbrev-prefix-mark (unrelated) ("C-M-#" . consult-register) ;; Other custom bindings ("M-y" . consult-yank-pop) ; orig. yank-pop ;; M-g bindings in `goto-map' ("M-g e" . consult-compile-error) ("M-g f" . consult-flymake) ; Alternative: consult-flycheck ("M-g g" . consult-goto-line) ; orig. goto-line ("M-g M-g" . consult-goto-line) ; orig. goto-line ("M-g o" . consult-outline) ; Alternative: consult-org-heading ("M-g m" . consult-mark) ("M-g k" . consult-global-mark) ("M-g i" . consult-imenu) ("M-g I" . consult-imenu-multi) ;; M-s bindings in `search-map' ("M-s d" . consult-find) ("M-s D" . consult-locate) ("M-s g" . consult-grep) ("M-s G" . consult-git-grep) ("M-s r" . consult-ripgrep) ("M-s l" . consult-line) ("M-s L" . consult-line-multi) ("M-s k" . consult-keep-lines) ("M-s u" . consult-focus-lines) ;; Isearch integration ("M-s e" . consult-isearch-history) :map isearch-mode-map ("M-e" . consult-isearch-history) ; orig. isearch-edit-string ("M-s e" . consult-isearch-history) ; orig. isearch-edit-string ("M-s l" . consult-line) ; needed by consult-line to detect isearch ("M-s L" . consult-line-multi) ; needed by consult-line to detect isearch ;; Minibuffer history :map minibuffer-local-map ("M-s" . consult-history) ; orig. next-matching-history-element ("M-r" . consult-history))) ; orig. previous-matching-history-element ;; Consult-specific Embark actions (use-package embark-consult :after (embark consult) :hook (embark-collect-mode . consult-preview-at-point-mode)) ;; In-buffer completion (use-package corfu :defer 3 :elpaca (corfu :files (:defaults "extensions/*")) :bind (:map corfu-map ("M-SPC" . corfu-insert-separator)) :config (global-corfu-mode) (corfu-popupinfo-mode) :custom (corfu-min-width 60) (corfu-max-width corfu-min-width) (corfu-auto t) (corfu-auto-delay 0.3) (corfu-cycle t) (corfu-popupinfo-delay 0.6) (corfu-separator ?\s)) (use-package kind-icon :after (corfu) :config (push #'kind-icon-margin-formatter corfu-margin-formatters) :custom (kind-icon-default-face 'corfu-default) (kind-icon-blend-background nil)) ;; Git (use-package magit :bind ("C-M-;" . magit-status) :custom (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1) (transient-history-file (concat xdg_cache_home "transient-history"))) (use-package git-timemachine :bind ("C-M-'" . git-timemachine)) ;; Project Management (use-package projectile :defer 1 :bind ("C-c p" . projectile-command-map) :config (push "*node_modules" projectile-globally-ignored-directories) (projectile-mode) :custom (projectile-known-projects-file (concat xdg_cache_home "projectile-bookmarks"))) (use-package treemacs :defer 1 :bind (:map global-map ("M-0" . treemacs-select-window) ("C-x t 1" . treemacs-delete-other-windows) ("C-x t t" . treemacs) ("C-x t B" . treemacs-bookmark) ("C-x t C-t" . treemacs-find-file) ("C-x t M-t" . treemacs-find-tag)) :config (treemacs-filewatch-mode) (treemacs-git-commit-diff-mode) :custom (treemacs-git-mode 'deferred) (treemacs-persist-file (concat xdg_cache_home "treemacs-persist"))) (use-package consult-projectile :after (consult projectile)) (use-package treemacs-magit :after (treemacs magit)) (use-package treemacs-projectile :after (treemacs projectile)) ;;; Language support ;; Auto-format (use-package apheleia :defer 3 :config (apheleia-global-mode)) (use-package reformatter ; required for zig fmt :after (zig-mode)) ;; Templates (use-package tempel :init ;; Completion at point (defun tempel-setup-capf () (setq-local completion-at-point-functions (cons #'tempel-expand ; or `tempel-complete' w/ `tempel-trigger-prexfix' completion-at-point-functions))) ;; :custom (tempel-trigger-prefix "<") :bind (("M-+" . tempel-complete) ; or `tempel-expand' ("M-*" . tempel-insert)) :hook ((conf-mode lsp-mode text-mode) . tempel-setup-capf)) (use-package tempel-collection :after (tempel)) (use-package yasnippet :bind ("C-c y s" . yas-insert-snippet) ("C-c y v" . yas-visit-snippet-file) :config (yas-reload-all) :hook (lsp-mode . yas-minor-mode)) (use-package yasnippet-snippets :after (yasnippet)) (use-package consult-yasnippet :after (yasnippet consult)) ;; Visualize color names (use-package rainbow-mode :hook ((conf-mode lsp-mode) . rainbow-mode)) ;; Syntax parsing/highlighting (use-package treesit-auto :defer 3 :config (global-treesit-auto-mode) :custom (treesit-auto-install 'prompt)) ;; Syntax checking (use-package flycheck :hook (lsp-mode . flycheck-mode) :custom (flycheck-python-flake8-executable "flake8")) (use-package consult-flycheck :after (consult flycheck)) ;; LSP (use-package lsp-mode :hook (((c-ts-mode c++-ts-mode go-ts-mode python-ts-mode zig-mode) . lsp) (lsp-mode . lsp-enable-which-key-integration)) :custom (lsp-idle-delay 0.6) (lsp-prefer-flymake nil) (lsp-session-file (concat xdg_cache_home "lsp-session"))) (use-package lsp-ui :hook (lsp-mode . lsp-ui-mode) :custom (lsp-ui-doc-position 'bottom-and-right) (lsp-ui-flycheck-enable t) (lsp-ui-flycheck-list-position 'right) (lsp-ui-peek-enable t) (lsp-ui-sideline-delay 0.6) (lsp-ui-sideline-show-code-actions t) (lsp-ui-sideline-show-hover t) (lsp-ui-sideline-update-mode 'line)) (use-package consult-lsp :after (consult lsp-mode) :bind (:map lsp-mode-map ([remap xref-find-apropos] . consult-lsp-symbols))) (use-package lsp-treemacs :after (lsp-mode treemacs)) ;; Debugging (use-package dap-mode :after (lsp-mode) :bind (:map dap-mode-map ("" . dap-debug) ("" . dap-next) ("" . dap-step-in) ("S-" . dap-disconnect) ("C-S-" . dap-debug-restart) ("C-c d" . dap-debug-last)) :config (eval-when-compile (require 'cl-lib)) (require 'dap-gdb-lldb) (require 'dap-lldb) (require 'dap-python) :custom (dap-auto-configure-features '(sessions locals tooltip)) (dap-breakpoints-file (concat xdg_cache_home "dap-breakpoints")) (dap-lldb-debug-program 'lldb-vscode) (dap-utils-extension-path (concat user-emacs-directory "dap-extensions/")) (dap-python-debugger 'debugpy) ;; Templates (dap-register-debug-template "Rust :: LLDB Run Configuration" (list :type "lldb" :request "launch" :name "LLDB::Run" :gdbpath "rust-lldb"))) (use-package realgud :defer t) (use-package realgud-lldb :defer t) ;; Go (use-package go-mode :hook (go-ts-mode . go-mode) :config (add-hook 'before-save-hook #'lsp-organize-imports)) ;; JavaScript (use-package js2-mode :interpreter (("nodejs" . js2-mode) ("node" . js2-mode)) :hook (js-ts-mode . js2-minor-mode) :custom (js-indent-level 2) (js2-mode-show-strict-warnings t)) ;; Python (use-package lsp-pyright :after (python) :hook (python-ts-mode . (lambda () (setq-local fill-column 80) (require 'lsp-pyright) (lsp)))) ; or `lsp-deferred' (use-package numpydoc :after (python) :bind (:map python-mode-map ("C-c C-n" . numpydoc-generate)) :custom (numpydoc-insert-examples-block nil)) (use-package conda :after (python) :init (setq conda-anaconda-home (expand-file-name "/opt/miniconda3") conda-env-home-directory (expand-file-name "~/.conda/envs")) (conda-env-initialize-interactive-shells) ;; (conda-env-autoactivate-mode) :hook ('find-file-hook . (lambda () (when (bound-and-true-p conda-project-env-path) (conda-env-activate-for-buffer))))) ;; Web templates (use-package web-mode :custom (web-mode-attribute-indent-offset 2) (web-mode-code-indent-offset 2) (web-mode-enable-front-matter-block t) (web-mode-markup-indent-offset 2) :mode ((("\\.astro\\'" "\\.html?\\'") . web-mode))) ;; Zig (use-package zig-mode :commands (zig-mode) :config (setq-local fill-column 100)) ;;; File format/markup support (use-package markdown-mode :commands (markdown-mode) :config (setq-local fill-column 120)) (use-package yaml-pro :hook (yaml-ts-mode . yaml-pro-ts-mode)) ;;; init.el ends here