;; 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.5) (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 ;;; General Options ;; Discard external customizations (setq-default custom-file null-device) ;; Don't ask about running processes on exit (setq confirm-kill-processes nil) ;; Better scrolling (setq scroll-step 1 scroll-conservatively 1000 scroll-preserve-screen-position t mouse-wheel-follow-mouse 't mouse-wheel-progressive-speed nil ;; 1st #: lines to scroll, 2nd #: while holding shift mouse-wheel-scroll-amount '(2 ((shift) .1))) (when (fboundp 'pixel-scroll-precision-mode) (pixel-scroll-precision-mode t)) ;; Tabs (setq tab-always-indent 'complete) (setq-default tab-width 4 indent-tabs-mode nil) ;; Line/Column numbers (setq column-number-mode t) (dolist (hook '(conf-mode-hook prog-mode-hook text-mode-hook)) (add-hook hook #'display-line-numbers-mode)) ;; Disable suspend-frame shortcut (global-set-key (kbd "") nil) ;; Escape behaves like C-g (global-set-key (kbd "") 'keyboard-escape-quit) ;; 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) ;; More reasonable keybinds for resizing windows (global-set-key (kbd "S-C-") 'shrink-window-horizontally) (global-set-key (kbd "S-C-") 'enlarge-window-horizontally) (global-set-key (kbd "S-C-") 'shrink-window) (global-set-key (kbd "S-C-") 'enlarge-window) ;; Built-in C settings (setq-default c-basic-offset 2 c-default-style "k&r") ;;; 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)) (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 80)) (use-package elec-pair :elpaca nil :config (electric-pair-mode)) ;; Move auto saves to XDG_CACHE_HOME (use-package files :elpaca nil :init (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))) :config (setq delete-old-versions t kept-new-versions 5 kept-old-versions 3 require-final-newline t version-control t)) (use-package paren :elpaca nil :config (show-paren-mode)) (use-package recentf :elpaca nil :init (setq recentf-save-file (expand-file-name "recentf" xdg-cache-home)) :config (setq recentf-auto-cleanup 'never recentf-max-menu-items 15 recentf-max-saved-items 100) (recentf-mode)) (use-package savehist :elpaca nil :init (setq savehist-file (concat xdg-cache-home "history")) :config (setq savehist-autosave-interval 60 savehist-additional-variables '(search-ring regexp-search-ring)) (savehist-mode)) (use-package saveplace :elpaca nil :init (setq save-place-file (concat xdg-cache-home "places")) :config (save-place-mode)) (use-package whitespace :elpaca nil :init (dolist (hook '(conf-mode-hook prog-mode-hook text-mode-hook)) (add-hook hook #'whitespace-mode)) (add-hook 'before-save-hook #'whitespace-cleanup) :config (setq whitespace-line-column 100 whitespace-style '(big-indent face lines-tail missing-newline-at-eof tabs trailing))) (elpaca-wait) ;;; Packages ;; 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 ((line (face-attribute 'mode-line :foreground)) (inactive (face-attribute 'mode-line-inactive :background))) (set-face-attribute 'mode-line nil :overline line) (set-face-attribute 'mode-line-inactive nil :overline line) (set-face-attribute 'mode-line-inactive nil :underline 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 inactive))) ;; Mode line (use-package minions :defer 1 :config (minions-mode)) (use-package keycast :defer t :config (setq keycast-mode-line-insert-after 'moody-mode-line-buffer-identification)) (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)) ;; Show keybind hints (use-package which-key :defer 1 :config (setq which-key-idle-delay 0.3) (which-key-mode)) ;; Better annotations (use-package marginalia :defer 1 :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :config (marginalia-mode)) ;; Better undo/redo (use-package undo-tree :defer 3 :config (setq undo-tree-auto-save-history t undo-tree-history-directory-alist `((".*" . ,(concat xdg-cache-home "undo-tree/")))) (global-undo-tree-mode)) ;; Completion style (use-package orderless :custom (completion-styles '(orderless basic)) (completion-category-overrides '((file (styles basic partial-completion))))) ;; Completion UI (use-package vertico :init (vertico-mode) :config (setq 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) :config (setq 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 :defer 3 :hook (embark-collect-mode . consult-preview-at-point-mode)) ;; In-buffer completion (use-package corfu :defer 3 :elpaca (corfu :files (:defaults "extensions/*")) :init (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) :bind (:map corfu-map ("M-SPC" . corfu-insert-separator))) (use-package kind-icon :after corfu :init (setq svg-lib-icons-dir (concat xdg-cache-home "svg-lib/")) :config (setq kind-icon-default-face 'corfu-default kind-icon-blend-background nil) (push #'kind-icon-margin-formatter corfu-margin-formatters)) ;; Git (use-package magit :init (setq transient-history-file (concat xdg-cache-home "transient-history")) :bind ("C-M-;" . magit-status) :commands (magit-status magit-get-current-branch) :custom (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)) (use-package git-timemachine :bind ("C-M-'" . git-timemachine)) ;; Project Managment (use-package projectile :defer 1 :init (setq projectile-known-projects-file (concat xdg-cache-home "projectile-bookmarks")) :bind ("C-c p" . projectile-command-map) :config (push "*node_modules" projectile-globally-ignored-directories) (projectile-mode)) (use-package treemacs :defer 1 :init (setq treemacs-persist-file (concat xdg-cache-home "treemacs-persist")) :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 (setq treemacs-git-mode 'deferred) (treemacs-filewatch-mode) (treemacs-git-commit-diff-mode)) (use-package consult-projectile) (use-package treemacs-magit) (use-package treemacs-projectile) ;;; Language support ;; Auto-format (use-package apheleia :defer 5 :config (apheleia-global-mode)) (use-package reformatter ; required for zig fmt :after zig-mode) ;; Visualize color names (use-package rainbow-mode :hook ((conf-mode prog-mode) . rainbow-mode)) ;; Syntax parsing/highlighting (use-package treesit-auto :defer 3 :config (setq treesit-auto-install 'prompt) (global-treesit-auto-mode)) ;; Syntax checking (use-package flycheck :hook (lsp-mode . flycheck-mode) :config (setq flycheck-python-flake8-executable "flake8")) (use-package consult-flycheck) ;; LSP (use-package lsp-mode :commands lsp lsp-deferred :hook (((c-mode c++-mode python-mode zig-mode) . lsp) (lsp-mode . lsp-enable-which-key-integration)) :config (setq lsp-idle-delay 0.6 lsp-prefer-flymake nil)) (use-package lsp-ui :hook (lsp-mode . lsp-ui-mode) :config (setq lsp-ui-doc-position 'bottom-and-right 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 lsp-ui-flycheck-enable t lsp-ui-flycheck-list-position 'right lsp-ui-peek-enable t)) (use-package consult-lsp :bind (:map lsp-mode-map ([remap xref-find-apropos] . consult-lsp-symbols))) (use-package lsp-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)) :config (setq dap-auto-configure-features '(sessions locals tooltip) dap-lldb-debug-program "/usr/bin/lldb-vscode" dap-python-debugger 'debugpy) (eval-when-compile (require 'cl-lib)) (require 'dap-python) (require 'dap-lldb) (require 'dap-gdb-lldb) (dap-gdb-lldb-setup) ;; Templates (dap-register-debug-template "Rust :: LLDB Run Configuration" (list :type "lldb" :request "launch" :name "LLDB::Run" :gdbpath "rust-lldb"))) (use-package realgud) (use-package realgud-lldb) ;; Zig (use-package zig-mode :init (push '(zig-mode . "zig") lsp-language-id-configuration) (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection "/usr/bin/zls") :major-modes '(zig-mode) :server-id 'zls)) :hook (zig-mode . (lambda () (setq-local fill-column 100) (setq-local tab-width 4)))) ;;; File format/markup support (use-package markdown-mode) (use-package toml-mode) (use-package yaml-pro) ;;; init.el ends here