;;; init.el --- Emacs configuration. -*- lexical-binding: t -*- ;;; Commentary: ;;; Code: (startup-redirect-eln-cache (expand-file-name "emacs/eln" (or (getenv "XDG_CACHE_HOME") "~/.cache/"))) (add-hook 'after-init-hook #'native-compile-prune-cache) ;; Load settings set through Custom. ;; (setq custom-file (locate-user-emacs-file "custom.el")) ;; (when (file-readable-p custom-file) ;; (load custom-file)) (defun tw/xdg-emacs-subdir (type name &optional create) "Get the name of a file or directory called NAME under $XDG__HOME/emacs. If CREATE is true and the resulting directory does not exist, create it." (let ((dir (expand-file-name (concat "emacs/" (string-trim-right name "/")) (pcase type ('cache (or (getenv "XDG_CACHE_HOME") "~/.cache/")) ('config (or (getenv "XDG_CONFIG_HOME") "~/.config/")) ('data (or (getenv "XDG_DATA_HOME") "~/.local/share/")) ;; The following two are Guix/GuixSD extensions. ('log (or (getenv "XDG_LOG_HOME") "~/.local/var/log/")) ('state (or (getenv "XDG_STATE_HOME") "~/.local/var/lib/")) (_ (error "Unknown XDG directory type: %S" type)))))) (when (and create (not (file-accessible-directory-p dir))) (make-directory dir t)) dir)) ;; Global/built-in Custom settings ;; Apply these as early as possible so that e.g. the native-comp files go to the right place. (mapc (apply-partially #'apply #'customize-set-variable) `((native-comp-async-report-warnings-errors silent "Don't pop up Warnings buffer for native compilation.") ;; Emacs GUI customization. (inhibit-startup-screen t "Don't show the startup screen with help links.") (menu-bar-mode nil "Hide the menu bar globally.") (tool-bar-mode nil "Hide the tool bar globally.") (tooltip-mode nil "Show tooltips in the echo area instead.") (max-mini-window-height 3 "Let the echo area grow to a maximum of 3 lines, e.g. when using `eldoc-mode'.") (scroll-up-aggressively 0.0 "Don't recenter the window if the point moves off the page.") (scroll-down-aggressively 0.0 "Don't recenter the window if the point moves off the page.") (pixel-scroll-precision-mode t "Enable pixel-by-pixel scrolling, e.g. to handle inline images.") ;; Niceties. (tramp-default-method "scpx" "ssh and scp hang forever. scpx is faster than sshx for large files.") (global-hl-line-mode t "Highlight the current line in all buffers.") (indicate-empty-lines t "Show a little marker in the margin for lines past EOF.") (column-number-mode t "Show the column number in the statusline.") (electric-pair-mode t "Auto-pair suitable characters like parentheses.") (completion-cycle-threshold 6 "Allow cycling through completions if there are 6 or fewer of them.") (shell-kill-buffer-on-exit t "Kill *shell* buffers as soon as their shell session exits.") ;; Indentation, formatting. (indent-tabs-mode nil "Always use spaces to indent.") (sentence-end-double-space nil "Use a single space after a sentence.") (fill-column 78 "Make hard-wrapped text a bit wider.") (require-final-newline t "Always add a final newline on save, if there is none.") ;; Make Emacs a good Guix citizen. (package-archives nil "Don't fetch packages from the internet; only get them from Guix.") ;; Default mode. (major-mode text-mode "Use `text-mode' by default in new buffers, not `fundamental-mode'.") ;; Recent files and history. Keep them out of ~/.config. (package-user-dir ,(tw/xdg-emacs-subdir 'data "elpa") "Save ELPA-related files here.") (auto-save-list-file-prefix ,(tw/xdg-emacs-subdir 'data "auto-save-list/saves-") "Put auto-save lists here.") (make-backup-files nil "Don't create backup files. They're annoying.") (backup-directory-alist (("." . ,(tw/xdg-emacs-subdir 'data "backup"))) "Put backup files in a sensible place.") (backup-by-copying t "Avoid breaking hardlinks when making backup files.") (auto-save-file-name-transforms ;; `file-name-as-directory' is important, since Emacs takes the directory part when UNIQUIFY is t. ((".*" ,(file-name-as-directory (tw/xdg-emacs-subdir 'data "auto-save" t)) t)) "Put auto-save #files# in a sensible directory.") (recentf-max-saved-items 1000 "Save lots of recently-opened files.") (recentf-save-file ,(tw/xdg-emacs-subdir 'data "recentf.el") "Save recently-opened files here.") (recentf-mode t "Save recently-opened files.") (savehist-file ,(tw/xdg-emacs-subdir 'data "savehist.el") "Save minibuffer history here.") (savehist-mode t "Save minibuffer history on quit."))) (defalias 'yes-or-no-p #'y-or-n-p "Always use `y-or-n-p' when asking for confirmation.") ;; Custom modes depending on file names. (mapc (apply-partially #'add-to-list 'auto-mode-alist) `((,(rx (or bos "/") "PKGBUILD" eos) . bash-ts-mode) (,(rx ".install" eos) . bash-ts-mode) (,(rx (or bos "/") "COMMIT_EDITMSG" eos) . diff-mode) ; useful for `git commit -v' (,(rx bos "/tmp/neomutt-") . mail-mode) (,(rx ".eml" eos) . mail-mode) (,(rx "." (1+ anything) "rc" eos) . conf-unix-mode))) (add-to-list 'magic-mode-alist `(,(rx "#!" (* (not space)) (? "env" (+ space) (? "-S" (+ space))) (or "guile" "racket")) . scheme-mode)) (add-hook 'mail-mode-hook #'auto-fill-mode) (defun tw/show-trailing-whitespace () "Highlight trailing spaces in the current buffer." (setq-local show-trailing-whitespace t)) (mapc (lambda (hook) (add-hook hook #'tw/show-trailing-whitespace)) '(prog-mode-hook conf-mode-hook yaml-mode-hook alidist-mode-hook)) (defun tw/enable-word-wrap () "Enable word wrapping." (toggle-word-wrap +1)) (add-hook 'markdown-mode-hook #'tw/enable-word-wrap) (add-hook 'org-mode-hook #'tw/enable-word-wrap) ;; `use-package' requirements. (require 'package) (package-initialize t) (eval-when-compile (require 'use-package)) (use-package diminish) ; for using :diminish later (use-package bind-key) ; for using :bind later ;; Some packages below have `:commands (...) :demand t'. ;; We need :commands for the byte-compiler, but we want to load the package immediately. ;; Look and feel (set-face-attribute 'default nil :family "Hermit" :height 100) ;; For some reason, Emacs doesn't detect italic support, and falls back to ;; underlining. Stop it from doing this and use italics instead. (set-face-attribute 'italic nil :slant 'italic :underline nil) (use-package catppuccin-theme :load-path "./" :custom (catppuccin-flavor 'mocha "Use the darkest Catppuccin theme.") (catppuccin-italic-comments t "Make comments italic. It looks nicer.") (catppuccin-italic-variables t "Make variable names italic. It looks nicer.") :config (catppuccin-reload)) (use-package smart-mode-line :hook (after-init . sml/setup) :custom (sml/no-confirm-load-theme t "Stop Emacs from constantly asking for user confirmation.") (sml/mode-width 'right "Move the minor-mode list to the right of the modeline.") (sml/theme 'respectful "Make `smart-mode-line' blend in with the active theme.")) ;; General editor behaviour. ;; TODO: Move from ivy + counsel to vertico + orderless + consult + marginalia ;; (+ embark?), to integrate better with vanilla Emacs and `completing-read'. ;; https://github.com/minad/vertico -- light completion engine ;; https://github.com/minad/vertico#child-frames-and-popups ;; https://github.com/minad/vertico#complementary-packages ;; https://github.com/minad/marginalia -- docstrings in M-x menu ;; https://github.com/oantolin/orderless -- regex search for vertico ;; https://github.com/minad/consult -- collection of commands using vertico ;; https://github.com/oantolin/embark -- make vertico better depending on thing at point (use-package ivy :commands (ivy-mode) :demand t :custom (ivy-use-selectable-prompt t "Allow selecting the ivy input as-is.") (ivy-use-virtual-buffers t "Show recentf and bookmarks in buffers list.") :config (ivy-mode +1) :diminish ivy-mode) (use-package counsel ; extra niceties for `ivy-mode' :after (ivy evil) ; evil for :bind-ing to :bind (("SPC" . counsel-M-x) ; doesn't work ("fr" . counsel-buffer-or-recentf) :map evil-visual-state-map ("SPC" . counsel-M-x)) :commands (counsel-mode) :demand t :config (counsel-mode +1) :diminish counsel-mode) (use-package dash-docs :custom (dash-docs-docsets-path (file-name-as-directory (tw/xdg-emacs-subdir 'data "dash-docsets" t)) "Store docsets in the XDG data directory.") (dash-docs-browser-func 'eww "Open documentation pages using `eww' instead of an external browser.") (dash-docs-enable-debugging nil "Disable popping up useless warnings.")) (defun tw/counsel-dash-is-help () "Install `counsel-dash-at-point' as `evil-lookup-func'." ;; Note: `evil-lookup-func' is already set to something else by ;; `tw/help-is-eldoc' for `eglot-mode'. (setq-local evil-lookup-func #'counsel-dash-at-point counsel-dash-docsets (cl-case major-mode ((python-mode python-ts-mode) '("Python 3")) (c++-mode '("C++")) (cmake-mode '("CMake")) (puppet-mode '("Puppet")) (tcl-mode '("Tcl")) (common-lisp-mode '("Common Lisp"))))) (use-package counsel-dash :after (dash-docs which-key) :commands (counsel-dash-at-point) :demand t :init (which-key-add-key-based-replacements "d" '("docs" . "Documentation")) :bind (("K" . counsel-dash-at-point) ("dK" . counsel-dash) ("di" . counsel-dash-install-docset) ("da" . counsel-dash-activate-docset) ("dd" . counsel-dash-deactivate-docset)) :hook ((python-mode python-ts-mode cmake-mode c++-mode puppet-mode tcl-mode common-lisp-mode) . tw/counsel-dash-is-help) :config ;; Activate all installed docsets by default. (setq counsel-dash-common-docsets (dash-docs-installed-docsets))) (use-package rainbow-mode :after (evil) :bind (("tR" . rainbow-mode))) (use-package form-feed :commands (global-form-feed-mode) :demand t :config (global-form-feed-mode +1) :diminish form-feed-mode) (use-package display-line-numbers ;; Included in Emacs >= 26. Better than `linum-mode'. ;; There is also `global-display-line-numbers-mode', but that also ;; enables line numbers in help windows, which I don't want. :hook (prog-mode conf-mode yaml-mode alidist-mode)) (use-package which-key :commands (which-key-mode) :demand t :config (which-key-mode +1) :diminish which-key-mode) (use-package undo-tree :after (evil) ; for our :bind-ing :bind (("U" . undo-tree-visualize)) :custom (undo-tree-history-directory-alist `(("." . ,(file-name-as-directory (tw/xdg-emacs-subdir 'data "undo-tree-history")))) "Store all `undo-tree' history in a single directory, instead of next to the associated file.") :commands (global-undo-tree-mode) :demand t ; this is required so that the :config stanza is actually run asap despite :bind :config (global-undo-tree-mode +1) :diminish undo-tree-mode) ;; IDE-like features. (use-package project :after (which-key evil) :init (which-key-add-key-based-replacements "p" '("project" . "Project")) :bind-keymap ("p" . project-prefix-map)) (use-package vc :after (which-key evil) :init (which-key-add-key-based-replacements "v" '("vc" . "Version control") "vM" '("merge" . "Version control merging")) :bind-keymap ("v" . vc-prefix-map)) (use-package log-edit :after (evil vc) :config (evil-define-key '(normal visual) log-edit-mode-map (kbd "\\") #'log-edit-done (kbd "a") #'log-edit-insert-changelog (kbd "d") #'log-edit-show-diff (kbd "f") #'log-edit-show-files (kbd "k") #'log-edit-kill-buffer (kbd "w") #'log-edit-generate-changelog-from-diff)) (use-package corfu ; https://github.com/minad/corfu :hook (prog-mode ledger-mode shell-mode eshell-mode) :custom (corfu-auto t "Show completion popup after a few seconds automatically.") :diminish corfu-mode) (use-package corfu-doc :after (corfu) :hook (corfu-mode) :diminish corfu-doc-mode) (use-package flyspell :hook mail-mode) (use-package flymake :after (evil which-key) :demand t ; needed for `flymake-collection' :hook (prog-mode yaml-mode alidist-mode) :init (which-key-add-key-based-replacements "e" '("errors" . "Flymake")) :bind (("eb" . flymake-start) ("ec" . display-local-help) ; Show the error message at point in the minibuffer. ; `flymake' also shows it using `eldoc', but documentation ; seems to override error messages. ; `flymake-show-diagnostic' only says "Nothing at point". ("el" . flymake-show-buffer-diagnostics) ("ep" . flymake-show-project-diagnostics) ("en" . flymake-goto-next-error) ("eN" . flymake-goto-prev-error) ("ev" . flymake-running-backends) ("eV" . flymake-disabled-backends)) :custom (flymake-suppress-zero-counters nil "Show severity counters even when they are zero.")) (use-package flymake-collection :after (flymake) :demand t ; we need it loaded now ;; This needs to be called in `after-init-hook' so that all other ;; packages' `:flymake-hook's are processed before f-c-hook-setup is ;; called. See https://github.com/mohkale/flymake-collection. :hook (after-init . flymake-collection-hook-setup)) ;; Language Server Protocol. (defun tw/help-is-eldoc (&rest _) "Set up `evil-lookup-func' to display the `eldoc' buffer." (when (eglot-managed-p) (setq-local evil-lookup-func #'eldoc-doc-buffer))) (use-package eglot ;; I have clang (for clangd) and python-lsp-server installed. ;; `:hook' adds `-mode' to the package name, but `eglot-mode' doesn't exist. :hook ((python-mode python-ts-mode c-mode c++-mode c-or-c++-ts-mode) . eglot-ensure) :commands (eglot) :functions (eglot-managed-p) :custom (eglot-autoshutdown t "Shut down language servers after deleting their last associated buffer.") (eglot-sync-connect 0.1 "Wait for the language server in the background if it takes longer than 100ms.") :config (add-hook 'eglot-managed-mode-hook #'tw/help-is-eldoc)) ;; Tree-sitter ;; TODO: Try any/all of the following new tree-sitter-based major modes. ;; Enable them using the following, replacing the relevant "old" major mode: ;; (add-to-list 'major-mode-remap-alist '(ruby-mode . ruby-ts-mode)) ;; New major mode 'css-ts-mode'. ;; New major mode 'json-ts-mode'. ;; New major mode 'dockerfile-ts-mode'. ;; New major mode 'ruby-ts-mode'. (mapc (lambda (dir) (add-to-list 'treesit-extra-load-path (file-name-as-directory (expand-file-name dir)))) '("/run/current-system/profile/lib/tree-sitter" "~/.guix-home/profile/lib/tree-sitter" "~/.guix-profile/lib/tree-sitter")) (use-package treesit :custom (treesit-font-lock-level 4 "Enable Angry Fruit Salad mode.")) (use-package c-ts-mode :init (add-to-list 'major-mode-remap-alist '(c-mode . c-ts-mode)) (add-to-list 'major-mode-remap-alist '(c++-mode . c++-ts-mode)) (add-to-list 'major-mode-remap-alist '(c-or-c++-mode . c-or-c++-ts-mode))) ;; Non-LSP language modes. (use-package cmake-ts-mode :mode (rx (or (: (or bos "/") "CMakeLists.txt") ".cmake") eos)) (use-package gnuplot :commands (gnuplot-mode gnuplot-make-buffer) :mode ((rx ".gnuplot" eos) . gnuplot-mode)) (use-package graphviz-dot-mode :mode (rx ".dot" eos) :custom (graphviz-dot-view-command "xdot %s" "Use xdot for previewing graphviz files.")) (use-package haskell-mode :mode (rx (or ".hs" ".lhs" ".hsc" ".cpphs" ".c2hs") eos)) (use-package hcl-mode :mode (rx "." (or "hcl" "nomad") eos)) (use-package mmm-mode :commands (mmm-mode) ;; Don't highlight submodes specially at all. The default background is annoying. :custom-face (mmm-default-submode-face ((t (:background unspecified))))) (use-package puppet-mode :mode (rx ".pp" eos)) (use-package python :after (flymake-collection) :commands (python-mode python-ts-mode) :mode (((rx ".py" (? (or ?\i ?\w)) eos) . python-ts-mode) ((rx ".aurora" eos) . python-ts-mode)) :flymake-hook (python-ts-mode flymake-collection-flake8 flymake-collection-mypy (flymake-collection-pycodestyle :disabled t) flymake-collection-pylint) (python-mode flymake-collection-flake8 flymake-collection-mypy (flymake-collection-pycodestyle :disabled t) flymake-collection-pylint)) (use-package rec-mode :mode (rx ".rec" eos)) (use-package sh-script ; built-in ;; Use `bash-ts-mode' instead of `sh-mode' if possible. ;; `bash-ts-mode' falls back to `sh-mode' if necessary. ;; Manually configuring :mode etc would be annoying, since there are a lot of entries. :config (add-to-list 'major-mode-remap-alist '(sh-mode . bash-ts-mode)) :custom (sh-basic-offset 2 "Use 2 spaces for `sh-mode' indents.")) (use-package tcl :mode ((rx ".tcl" eos) . tcl-mode) :magic ((rx "#%Module1.0") . tcl-mode)) (use-package web-mode :mode (rx "." (or "htm" "html" "js" "css" "scss") eos)) (use-package yaml-mode :mode (rx (or (seq ".y" (? "a") "ml") (seq "aliPublish" (* (not ?/)) ".conf")) eos)) (defun tw/ledger-format-on-save () "Re-indent the entire file." ;; Subset of `ledger-mode-clean-buffer'. That also sorts the buffer, which I don't want. (save-excursion (let ((start (point-min-marker)) (end (point-max-marker))) (untabify start end) (ledger-post-align-postings start end) (ledger-mode-remove-extra-lines)))) (defun tw/enable-ledger-format-on-save () "Enable reformating the open file on save." (add-hook 'before-save-hook #'tw/ledger-format-on-save 0 t)) (use-package ledger-mode :after (evil) :commands (ledger-mode) :mode (rx ".journal" eos) :hook (ledger-mode . tw/enable-ledger-format-on-save) :custom (ledger-default-date-format ledger-iso-date-format "Use hledger-style dates.") (ledger-reconcile-default-date-format ledger-iso-date-format "Use hledger-style dates.") (ledger-reconcile-default-commodity "€" "Make euros the default currency.") (ledger-post-account-alignment-column 2 "Use 2-space indents.") (ledger-post-amount-alignment-at :decimal "Align amounts at decimal points/commas.") (ledger-post-amount-alignment-column 52 "Align amounts' decimal points to the 52nd column.") (ledger-highlight-xact-under-point nil "Don't highlight the transaction at point.") :config (evil-define-key 'normal ledger-mode-map (kbd "TAB") #'ledger-indent-line)) (use-package lisp :init (which-key-add-key-based-replacements "k" '("sexp-nav" . "S-expression navigation")) :bind (("kl" . forward-sexp) ("kh" . backward-sexp) ("kL" . forward-list) ("kH" . backward-list) ("kj" . down-list) ("kk" . up-list) ("kK" . backward-up-list) ("kd" . kill-sexp) ("kD" . backward-kill-sexp) ("kb" . beginning-of-defun) ("kB" . beginning-of-defun-comments) ("ke" . end-of-defun) ("kv" . mark-sexp) ("kV" . mark-defun) ("kN" . narrow-to-defun) ("ks" . insert-pair) ("kr" . raise-sexp) ("kc" . check-parens))) (defun tw/resize-repl-window () "Make the REPL window small, so it stays out of the way." (shrink-window (- (window-height) 5))) (use-package geiser :after (evil) :commands (geiser geiser-eval-buffer geiser-eval-definition geiser-eval-region geiser-eval-last-sexp geiser-mode-switch-to-repl geiser-mode-switch-to-repl-and-enter) :hook ((scheme-mode . geiser-autodoc-mode) (geiser-repl-mode . tw/resize-repl-window)) :config (evil-define-key '(normal visual) scheme-mode-map (kbd "z") #'geiser-mode-switch-to-repl (kbd "Z") #'geiser-mode-switch-to-repl-and-enter (kbd "eb") #'geiser-eval-buffer (kbd "ef") #'geiser-eval-definition (kbd "er") #'geiser-eval-region (kbd "el") #'geiser-eval-last-sexp) :defines scheme-mode-map) (use-package geiser-guile :after (geiser)) (use-package sly :after (evil) :hook (lisp-mode . sly-mode) ; `common-lisp-mode' is `lisp-mode'. :config (evil-define-key '(normal visual) lisp-mode-map (kbd "C-c") #'sly-interrupt (kbd "z") #'sly (kbd "Z") #'sly-mrepl-sync (kbd "i") #'sly-inspect (kbd "D") #'sly-disassemble-symbol (kbd "E") #'sly-edit-value (kbd "eT") #'sly-list-threads ; eval requests get a new thread each (kbd "e:") #'sly-interactive-eval (kbd "el") #'sly-eval-last-expression (kbd "ep") #'sly-pprint-eval-last-expression (kbd "eb") #'sly-eval-buffer (kbd "ef") #'sly-eval-defun (kbd "er") #'sly-eval-region (kbd "eF") #'sly-compile-defun (kbd "eB") #'sly-compile-file (kbd "eL") #'sly-compile-and-load-file (kbd "eR") #'sly-compile-region (kbd "eU") #'sly-undefine-function (kbd "eM") #'sly-remove-method (kbd "dd") #'sly-describe-symbol (kbd "df") #'sly-describe-function (kbd "da") #'sly-apropos (kbd "dA") #'sly-apropos-all (kbd "dg") #'sly-edit-definition (kbd "dC-o") #'sly-pop-find-definition-stack (kbd "dG") #'sly-edit-uses (kbd "dwc") #'sly-who-calls (kbd "dwC") #'sly-calls-who (kbd "dwr") #'sly-who-references (kbd "dwb") #'sly-who-binds (kbd "dws") #'sly-who-sets (kbd "dwm") #'sly-who-macroexpands (kbd "dwS") #'sly-who-specializes (kbd "dhs") #'hyperspec-lookup ; hyperspec.el is bundled with sly; opens in browser (kbd "dhf") #'hyperspec-lookup-format (kbd "dhm") #'hyperspec-lookup-reader-macro (kbd "cl") #'sly-list-connections (kbd "cn") #'sly-next-connection (kbd "cp") #'sly-prev-connection (kbd "m1") #'sly-expand-1 (kbd "mm") #'sly-macroexpand-all (kbd "mf") #'sly-format-string-expand (kbd "tt") #'sly-trace-dialog-toggle-trace (kbd "ts") #'sly-trace-dialog (kbd "tf") #'sly-toggle-trace-fdefinition (kbd "tF") #'sly-untrace-all (kbd "ss") #'sly-stickers-dwim ; an ephemeral `print' around the thing at point (kbd "sr") #'sly-stickers-replay (kbd "sb") #'sly-stickers-toggle-break-on-stickers (kbd "sf") #'sly-stickers-fetch (kbd "sn") #'sly-stickers-next-sticker (kbd "sp") #'sly-stickers-prev-sticker (kbd "ta") #'sly-autodoc-mode) :custom (sly-mrepl-history-file-name (tw/xdg-emacs-subdir 'data "sly-mrepl-history"))) ;; Org-mode (use-package org :commands (org-mode) :mode ((rx ".org" eos) . org-mode) :custom (org-latex-src-block-backend 'minted "Colourise source code.") (org-latex-packages-alist '(("" "svg") ("" "minted")) "Use svg and syntax highlighting packages.") (org-latex-pdf-process '("latexmk -shell-escape -f -pdf -%latex -interaction=nonstopmode -output-directory=%o %f") "Allow -shell-escape needed by svg and minted packages.")) (use-package ob ; org-babel :after (org) :custom (org-confirm-babel-evaluate nil "Allow running code blocks without confirmation.") ;; List of supported languages: ;; https://orgmode.org/worg/org-contrib/babel/languages/index.html (org-babel-load-languages '((emacs-lisp . t) (lisp . t) (dot . t) (python . t) (rec . t)) ; see `ob-rec' below "Load bindings for more languages for use in #+begin_src blocks.")) (defun tw/latex-section-commands (name) "Create a pair of section commands like (\"\\NAME{%s}\" . \"\\NAME*{%s}\"). For use in `org-latex-classes'." (cons (format "\\%s{%%s}" name) (format "\\%s*{%%s}" name))) (defconst tw/latex-part (tw/latex-section-commands "part") "Part LaTeX commands for `org-latex-classes'.") (defconst tw/latex-chapter (tw/latex-section-commands "chapter") "Chapter LaTeX commands for `org-latex-classes'.") (defconst tw/latex-section-and-below (mapcar #'tw/latex-section-commands '("section" "subsection" "subsubsection" "paragraph" "subparagraph")) "Section to subparagraph LaTeX commands for `org-latex-classes'.") (use-package ox-latex ; org-export-latex :after (org) :custom (org-latex-classes `(("paperlike" "\\documentclass{paperlike}" . ,tw/latex-section-and-below) ("examtext" "\\documentclass{examtext}" . ,tw/latex-section-and-below) ("minutes" "\\documentclass{minutes}" . ,tw/latex-section-and-below) ("mapreport" "\\documentclass{mapreport}" ,tw/latex-chapter . ,tw/latex-section-and-below) ("pt3report" "\\documentclass{pt3report}" ,tw/latex-chapter . ,tw/latex-section-and-below) ("article" "\\documentclass{article}" . ,tw/latex-section-and-below) ("scrartcl" "\\documentclass{scrartcl}" . ,tw/latex-section-and-below) ("report" "\\documentclass{report}" ,tw/latex-part ,tw/latex-chapter . ,tw/latex-section-and-below) ("report-noparts" "\\documentclass{report}" ,tw/latex-chapter . ,tw/latex-section-and-below) ("book" "\\documentclass{book}" ,tw/latex-part ,tw/latex-chapter . ,tw/latex-section-and-below) ("book-noparts" "\\documentclass{book}" ,tw/latex-chapter . ,tw/latex-section-and-below) ("checklist" "\\documentclass{checklist}" . ,tw/latex-section-and-below)) "Define more documentclasses for org-latex.")) (use-package outline :commands (outline-mode outline-minor-mode) :custom ;; Mirror the default "C-c @" binding for `outline-minor-mode'. (outline-minor-mode-prefix (kbd "@") "Use localleader for `outline-minor-mode' keybindings.")) ;; My own custom packages, and stuff that isn't on MELPA. (use-package actionlint :after (flymake) :load-path "include/" :hook ((yaml-mode yaml-ts-mode) . actionlint-setup)) (use-package alidist-mode :after (flymake) :load-path "include/" :commands (alidist-mode) :mode (rx (or bot "/") "alidist/" (1+ (not ?\/)) ".sh" eot)) (use-package flymake-guile :after (flymake) :load-path "include/" :hook (scheme-mode . flymake-guile-enable)) (use-package bemscript-mode :load-path "include/" :mode (rx ".bem" eos)) (use-package ifm-mode :load-path "include/" :mode (rx ".ifm" eos)) (use-package pam-env-mode :load-path "include/" :mode (rx (or bos "/") (or "pam_env.conf" ".pam_environment") eos)) (use-package environmentd-mode :load-path "include/" :mode (rx (or bos "/") (or (: (? "etc/") "environment") (: ".environment.d/" (1+ (not ?\/)) ".conf")) eos)) (use-package ob-rec ;; `org-babel' hooks for `rec-mode' :after (org ob rec-mode) :load-path "include/") (use-package vcard-mode :load-path "include/" :mode (rx "." (or "vcf" "vcard") eos)) ;; Vim keybindings. (defun tw/switch-to-other-buffer () "Switch to the last-used buffer." (interactive) (switch-to-buffer (other-buffer))) (defun tw/new-buffer () "Open a new, empty buffer." (interactive) (switch-to-buffer (generate-new-buffer "untitled"))) (defun tw/delete-current-buffer-file () "Ask for confirmation, then delete the file associated with the current buffer." (interactive) (let ((buffer (current-buffer))) (when (yes-or-no-p (concat "Delete `" (buffer-file-name buffer) "'?")) (delete-file (buffer-file-name buffer)) (kill-buffer buffer)))) (use-package evil :after (which-key) :commands (evil-mode evil-ex-nohighlight) :init (setq evil-want-keybinding nil) ; evil-collection needs this :custom (evil-undo-system 'undo-tree "Use `undo-tree' for evil's undo-redo function.") (evil-want-minibuffer t "Use evil bindings in the minibuffer too.") (evil-want-C-u-scroll t "Scroll on C-u in normal mode, not `universal-argument'.") (evil-want-C-u-delete t "Delete line on C-u in insert mode, not `universal-argument'.") (evil-want-Y-yank-to-eol t "Yank from point to end-of-line on Y.") (evil-symbol-word-search t "Always search by full variable names when using * and #.") :config (evil-mode +1) (evil-set-leader '(normal visual) (kbd "SPC")) ; (evil-set-leader '(normal visual) (kbd "\\") t) ; (evil-define-key '(normal motion) diff-mode-shared-map ; not `diff-mode-map', else toggling `read-only-mode' destroys the binding (kbd "\\") #'read-only-mode) ; mirror default binding from evil-collection (evil-define-key '(normal insert visual replace) 'global (kbd "C-s") #'save-buffer) ;; Global major-mode-independent keys should be defined here. Major ;; mode-dependent keys (e.g. for launching a REPL) should go under ;; instead. Use `use-package' `:bind' for those. (evil-define-key '(normal visual) 'global ;; These keybindings mirror the default Spacemacs ones because I have ;; muscle memory of those. (kbd ":") #'eval-expression (kbd "TAB") #'tw/switch-to-other-buffer (kbd "bb") #'switch-to-buffer (kbd "bd") #'kill-current-buffer (kbd "bn") #'tw/new-buffer (kbd "br") #'revert-buffer-quick (kbd "bs") #'scratch-buffer (kbd "bw") #'read-only-mode (kbd "bx") #'kill-buffer-and-window (kbd "fd") #'tw/delete-current-buffer-file (kbd "ff") #'find-file (kbd "fR") #'rename-visited-file (kbd "fs") #'save-buffer (kbd "h") help-map (kbd "hw") #'which-key-show-top-level (kbd "sc") #'evil-ex-nohighlight (kbd "tf") #'auto-fill-mode (kbd "tl") #'toggle-truncate-lines (kbd "tn") #'display-line-numbers-mode (kbd "u") #'universal-argument (kbd "w") evil-window-map (kbd "wd") #'evil-window-delete ; analogous to "bd" (kbd "wx") #'kill-buffer-and-window) ; analogous to "bx" (which-key-add-key-based-replacements ;; Names are a `cons' of a short name and a long name. ;; E.g. for b, "buffer" is shown under "b" in the "" menu, ;; while "Buffers" is shown as the title in the "b" menu. "b" '("buffer" . "Buffers") "f" '("file" . "Files") "h" '("help" . "General help and documentation") "q" '("quit" . "Finish editing the current buffer in emacsclient") "s" '("search" . "Search operations and options") "t" '("toggle" . "Toggles and quick settings") "w" '("window" . "Windows")) :functions (evil-define-key evil-set-leader evil-define-key* evil-window-delete evil-delay) :defines (evil-visual-state-map)) (use-package evil-collection :after (evil) :commands (evil-collection-init) :demand t :config (evil-collection-init) :diminish evil-collection-unimpaired-mode :custom ;; Without `evil-collection-key-blacklist', in `diff-mode', space isn't ;; assigned to the leader key automatically, unlike in other modes. (evil-collection-key-blacklist '("SPC" "\\") "Don't bind to our leader keys at all.") (evil-collection-setup-minibuffer t "Use evil-collection in minibuffer to match `evil-want-minibuffer'.")) (use-package evil-org :after (evil org) :hook org-mode :config (evil-define-key '(normal visual) org-mode-map (kbd "\\") #'org-ctrl-c-ctrl-c (kbd "ib") #'org-insert-structure-template (kbd "id") #'org-insert-drawer (kbd "iD") #'org-insert-time-stamp (kbd "ih") #'org-insert-heading (kbd "iH") #'org-insert-subheading (kbd "it") #'org-insert-todo-heading (kbd "iT") #'org-insert-todo-subheading (kbd "ii") #'org-insert-item (kbd "il") #'org-insert-link (kbd "p") #'org-set-property (kbd "t") #'org-set-tags ;; Source code block editing (kbd "'") #'org-edit-src-code (kbd "e") #'org-export-dispatch) (evil-define-key '(normal visual) org-src-mode-map (kbd "'") #'org-edit-src-exit (kbd "\\") #'org-edit-src-save (kbd "a") #'org-edit-src-abort)) (use-package evil-replace-with-register :after (evil) :commands (evil-replace-with-register-install) :demand t ;; :custom (evil-replace-with-register-key "gR" "Use the default key.") :config (evil-replace-with-register-install)) (use-package evil-commentary ; e.g. "gcc" / "gcap" to comment out blocks of text :after (evil) :commands (evil-commentary-mode) :demand t :config (evil-commentary-mode +1) :diminish evil-commentary-mode) (use-package evil-expat ; for :reverse, :remove, :rename, :colo, :g*, ... ex commands :after (evil)) (use-package evil-surround :after (evil) :commands (global-evil-surround-mode) :demand t :config (global-evil-surround-mode +1)) (use-package smartparens ; required by evil-cleverparens :custom ;; (sp-sexp-prefix '() "Set up Guix gexp-related sexp prefixes.") ) (use-package evil-cleverparens :after (evil smartparens) :hook ((lisp-mode lisp-data-mode scheme-mode) . evil-cleverparens-mode)) (use-package evil-multiedit ;; See: https://github.com/hlissner/evil-multiedit#usage :commands (evil-multiedit-default-keybinds) :demand t :config (evil-multiedit-default-keybinds)) (use-package evil-args :after (evil) :config ;; Bind evil-args text objects only. ;; See https://github.com/wcsmith/evil-args for more bindings. (define-key evil-inner-text-objects-map "a" 'evil-inner-arg) (define-key evil-outer-text-objects-map "a" 'evil-outer-arg)) (use-package evil-numbers :after (evil) :bind (("+" . evil-numbers/inc-at-pt) ("-" . evil-numbers/dec-at-pt))) (use-package evil-goggles ; visual previews for edit operations :after (evil) :commands (evil-goggles-mode evil-goggles-use-diff-faces) :demand t :config (evil-goggles-mode +1) (evil-goggles-use-diff-faces) :diminish evil-goggles-mode) (use-package evil-traces ; visual previews for :ex commands :after (evil) :commands (evil-traces-mode evil-traces-use-diff-faces) :demand t :config (evil-traces-mode +1) (evil-traces-use-diff-faces) :diminish evil-traces-mode) (use-package evil-markdown :after (evil markdown-mode) :hook markdown-mode) ;; Lots of useful text objects and keybinds: ;; https://github.com/iyefrat/evil-tex#incomplete-showcase (use-package evil-tex :after (evil tex-mode) :hook TeX-mode) (use-package evil-text-object-python :after (evil python) :hook (python-mode . evil-text-object-python-add-bindings)) ;; Lisp features (use-package aggressive-indent :hook (scheme-mode emacs-lisp-mode common-lisp-mode sh-mode bash-ts-mode)) (use-package inf-lisp :custom (inferior-lisp-program "sbcl")) (defun tw/lisp-evil-setup () "Set up evil in general `lisp-mode' buffers." ;; https://github.com/wcsmith/evil-args#customization (setq-local evil-args-delimiters '(" "))) ;; Sadly, not all Lisp modes derive from `lisp-mode'. (add-hook 'lisp-mode-hook #'tw/lisp-evil-setup) (add-hook 'lisp-data-mode-hook #'tw/lisp-evil-setup) ; for elisp (add-hook 'scheme-mode-hook #'tw/lisp-evil-setup) ;; buffer-locally set `evil-lookup-func' (used on K keys) for ;; languages where something better than man pages is available ;; (e.g. `describe-symbol' for elisp). (defun tw/elisp-lookup-func () "Show help in `emacs-lisp-mode' buffers." (let ((sym (symbol-at-point))) (if sym (describe-symbol sym) (call-interactively #'describe-symbol)))) (defun tw/emacs-lisp-evil-setup () "Set up evil in `emacs-lisp-mode' buffers." (setq-local evil-lookup-func #'tw/elisp-lookup-func)) (add-hook 'emacs-lisp-mode-hook #'tw/emacs-lisp-evil-setup) (evil-define-key '(normal visual) emacs-lisp-mode-map (kbd "eb") #'eval-buffer (kbd "ef") #'eval-defun (kbd "er") #'eval-region (kbd "el") #'eval-last-sexp (kbd "ep") #'eval-print-last-sexp) ;; Guix-related .dir-locals.el entries. These are fine; don't prompt every time. (add-to-list 'safe-local-variable-values '(geiser-repl-per-project-p . t)) (mapc (apply-partially #'add-to-list 'safe-local-eval-forms) '((modify-syntax-entry 126 "'") (modify-syntax-entry 36 "'") (modify-syntax-entry 43 "'") (let ((root-dir-unexpanded (locate-dominating-file default-directory ".dir-locals.el"))) (when root-dir-unexpanded (let* ((root-dir (file-local-name (expand-file-name root-dir-unexpanded))) (root-dir* (directory-file-name root-dir))) (unless (boundp 'geiser-guile-load-path) (defvar geiser-guile-load-path 'nil)) (make-local-variable 'geiser-guile-load-path) (require 'cl-lib) (cl-pushnew root-dir* geiser-guile-load-path :test #'string-equal)))) (progn (require 'lisp-mode) (defun emacs27-lisp-fill-paragraph (&optional justify) (interactive "P") (or (fill-comment-paragraph justify) (let ((paragraph-start (concat paragraph-start "\\|\\s-*\\([(;\"]\\|\\s-:\\|`(\\|#'(\\)")) (paragraph-separate (concat paragraph-separate "\\|\\s-*\".*[,\\.]$")) (fill-column (if (and (integerp emacs-lisp-docstring-fill-column) (derived-mode-p 'emacs-lisp-mode)) emacs-lisp-docstring-fill-column fill-column))) (fill-paragraph justify)) t)) (setq-local fill-paragraph-function #'emacs27-lisp-fill-paragraph)) ;; Forms used by Guix upstream. (add-to-list 'completion-ignored-extensions ".go") (setq-local guix-directory (locate-dominating-file default-directory ".dir-locals.el")) (with-eval-after-load 'yasnippet (let ((guix-yasnippets (expand-file-name "etc/snippets/yas" (locate-dominating-file default-directory ".dir-locals.el")))) (unless (member guix-yasnippets yas-snippet-dirs) (add-to-list 'yas-snippet-dirs guix-yasnippets) (yas-reload-all)))) (let ((root-dir-unexpanded (locate-dominating-file default-directory ".dir-locals.el"))) (when root-dir-unexpanded (let* ((root-dir (expand-file-name root-dir-unexpanded)) (root-dir* (directory-file-name root-dir))) (unless (boundp 'geiser-guile-load-path) (defvar geiser-guile-load-path 'nil)) (make-local-variable 'geiser-guile-load-path) (require 'cl-lib) (cl-pushnew root-dir* geiser-guile-load-path :test #'string-equal)))))) ;;; init.el ends here