dotfiles/dot_config/emacs/init.el

742 lines
23 KiB
EmacsLisp

;; init.el --- GNU Emacs Initialization File -*- lexical-binding: t; -*-
;; Copyright (c) 2023 Andrew Scott
;; Author: Andrew Scott <andy at andyscott dot me>
;; Keywords: convenience, tools
;; URL: https://codeberg.org/andyscott/dotfiles
;; This file is not part of GNU Emacs.
;;; Commentary:
;; My Emacs initialization file.
;;; 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.7)
(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))
;; Unload seq before elpaca build - also see `use-package' declaration for seq below
;; https://github.com/progfolio/elpaca/issues/216#issuecomment-1868444883
(defun +elpaca-unload-seq (e)
(and (featurep 'seq) (unload-feature 'seq t))
(elpaca--continue-build e))
(defun +elpaca-seq-build-steps ()
(append (butlast (if (file-exists-p (expand-file-name "seq" elpaca-builds-directory))
elpaca--pre-built-steps elpaca-build-steps))
(list '+elpaca-unload-seq 'elpaca--activate-package)))
;; 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
:ensure nil
:init
;; Initial mode
(setq initial-major-mode 'fundamental-mode)
;;; 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
("<escape>" . keyboard-escape-quit)
;; Resizing buffers
("S-C-<left>" . shrink-window-horizontally)
("S-C-<right>" . enlarge-window-horizontally)
("S-C-<down>" . shrink-window)
("S-C-<up>" . 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))
;; 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
:ensure nil
:commands (compile recompile)
:config (setq compilation-scroll-output t))
(use-package cus-edit
:ensure nil
:config (setq-default custom-file null-device))
(use-package display-fill-column-indicator
:ensure nil
:hook ((conf-mode
markdown-mode
prog-mode) . display-fill-column-indicator-mode)
:config (setq-default fill-column 100))
(use-package display-line-numbers
:ensure nil
:hook ((conf-mode prog-mode) . display-line-numbers-mode))
(use-package elec-pair
:ensure nil
:defer 3
:config (electric-pair-mode))
(use-package files
:ensure nil
:config
(let ((auto_save_directory (concat xdg_cache_home "auto-save-list/")))
(unless (file-directory-p auto_save_directory)
(make-directory auto_save_directory))
(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
:ensure nil
:defer 3
:hook
(((git-commit-mode
markdown-mode
text-mode) . flyspell-mode)
(lsp-mode . flyspell-prog-mode)))
(use-package mwheel
:ensure nil
:config
(setq mouse-wheel-follow-mouse t
mouse-wheel-progressive-speed nil
mouse-wheel-scroll-amount '(2 ((shift) . 1))))
(use-package paren
:ensure nil
:defer 3
:config (show-paren-mode))
(use-package recentf
:ensure 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
:ensure 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
:ensure nil
:defer 1
:config
(setq save-place-file (concat xdg_cache_home "places"))
(save-place-mode))
(use-package seq
; Unload seq before elpaca build
:ensure `(seq :build ,(+elpaca-seq-build-steps)))
(use-package simple
:ensure nil
:config
(setq-default indent-tabs-mode nil)
(column-number-mode))
(use-package whitespace
:ensure 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
:ensure (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_inactive)
(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
:ensure (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 transient)
(use-package magit
:after transient
: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
csharp-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)
:config (lsp-treemacs-sync-mode))
;; Debugging
(use-package dap-mode
:after (lsp-mode)
:bind
(:map dap-mode-map
("<f5>" . dap-debug)
("<f10>" . dap-next)
("<f11>" . dap-step-in)
("S-<f5>" . dap-disconnect)
("C-S-<f5>" . 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-css-indent-offset 2)
(web-mode-markup-indent-offset 2)
(web-mode-enable-front-matter-block t)
(web-mode-engines-alist '(("blade" . "\\.blade\\.")
("razor" . "\\.cshtml\\'")))
:mode
((("\\.astro\\'"
"\\.cshtml\\'"
"\\.html?\\'"
"\\.razor\\'") . web-mode)))
;; Zig
(use-package zig-mode
:commands (zig-mode)
:config
(defun zig-compile ()
(interactive)
(save-buffer)
(zig--run-cmd "build"))
(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