From de20fc8d904643ffe6957febfc6a24e57c12b512 Mon Sep 17 00:00:00 2001 From: Timo Wilken Date: Sat, 9 Mar 2024 14:52:56 +0100 Subject: Separate home service into PIM, dev env and graphical parts This means we only instantiate Shepherd and mcron services if we really need them, to avoid annoyance on servers. --- tw/services/files/emacs-init.el | 1027 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1027 insertions(+) create mode 100644 tw/services/files/emacs-init.el (limited to 'tw/services/files/emacs-init.el') diff --git a/tw/services/files/emacs-init.el b/tw/services/files/emacs-init.el new file mode 100644 index 00000000..0f20782d --- /dev/null +++ b/tw/services/files/emacs-init.el @@ -0,0 +1,1027 @@ +;;; 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.") + (tab-always-indent complete "Enable completion-on-tab.") + (completion-cycle-threshold 6 "Allow cycling through completions if there are 6 or fewer of them.") + (completion-styles (basic partial-completion) "Enable fast completion styles.") + (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. + +(use-package gcmh ; "garbage collector magic hack": run GC when not in focus + :config (gcmh-mode +1) + :diminish gcmh-mode) + +;; 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 + (lisp-mode '("Common Lisp")) + ((python-mode python-ts-mode) '("Python 3")) + (c++-mode '("C++")) + (cmake-mode '("CMake")) + (puppet-mode '("Puppet")) + (yaml-mode '("Ansible")) + (tcl-mode '("Tcl")) + (html-mode '("HTML" "CSS")) + ((css-mode css-ts-mode) '("CSS")) + (web-mode '("HTML" "CSS"))))) + +(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 (( lisp-mode python-mode python-ts-mode cmake-mode c++-mode puppet-mode yaml-mode + tcl-mode html-mode css-mode css-ts-mode web-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")) + (evil-define-key '(normal visual) 'global + (kbd "fp") #'project-find-file) ; also pf + :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 company + :config (global-company-mode +1) + :diminish company-mode) + +(use-package company-quickhelp + :after (company) + :config (company-quickhelp-mode +1) + :diminish company-quickhelp-mode) + +(use-package company-posframe + :after (company) + :config (company-posframe-mode +1) + :diminish company-posframe-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) + (eglot-managed-mode . tw/help-is-eldoc)) + :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.")) + +;; 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 '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.")) + +;; Non-LSP language modes. +(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))) + +(use-package cmake-ts-mode + :mode (rx (or (: (or bos "/") "CMakeLists.txt") ".cmake") eos)) + +(use-package json-ts-mode + :mode (rx ".json" eos) + :config + (evil-define-key '(normal visual) json-ts-mode-map + (kbd "==") #'json-pretty-print + (kbd "=b") #'json-pretty-print-buffer + (kbd "=o") #'json-pretty-print-ordered + (kbd "=B") #'json-pretty-print-buffer-ordered)) + +(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)) + :config + ;; Disable all flymake-collection linters in Python modes, since eglot/pylsp + ;; should take care of it. It doesn't do type checking, so enable mypy. + (cl-dolist (mode '(python-ts-mode python-mode)) + (add-to-list 'flymake-collection-config `(,mode flymake-mypy)))) + +(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) + :custom + (web-mode-css-indent-offset 2 "Indent CSS by two spaces.")) + +(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'. + (sly-mrepl-mode . tw/resize-repl-window)) + :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) + (gnuplot . 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-echo-state nil "Don't show the '--- INSERT ---' string in the echo area on evil state changes.") + (evil-undo-system 'undo-tree "Use `undo-tree' for evil's undo-redo function.") + (evil-search-module 'evil-search "Use evil's built-in search function, for search history support.") + (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 "td") #'toggle-debug-on-error + (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) + :custom + (evil-cleverparens-use-additional-movement-keys nil "Disable overriding of standard vim bracket navigation keys.")) + +(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 lisp-mode sh-mode bash-ts-mode)) + +(defun tw/find-asd-systems (directory) + "Return a list of Common Lisp .asd systems found in DIRECTORY." + (let ((asd-rx (rx ".asd" eos))) + ;; `locate-dominating-file' will call this function once with the original + ;; file name as DIRECTORY, but `directory-files' fails if its argument is + ;; a regular file, so protect against this. + (and (directory-name-p directory) + (mapcar (lambda (file) + (string-trim-right file asd-rx)) + (directory-files directory nil asd-rx))))) + +(defun tw/lisp-project-setup () + "Set up a Lisp REPL for the current project." + (when-let ((fname (buffer-file-name)) + (project-directory + (or (locate-dominating-file fname "guix.scm") + (locate-dominating-file fname #'tw/find-asd-systems) + (project-current nil (file-name-directory fname))))) + (cd project-directory) + (setq-local + inferior-lisp-program + `(;; If a guix.scm file exists, run Lisp in a Guix shell to get dependencies. + ,@(and (file-exists-p (file-name-concat project-directory "guix.scm")) + '("guix" "shell" "-Df" "guix.scm" "--")) + "sbcl" "--noinform" + ;; Load all defined asdf systems. + ,@(mapcan (lambda (system) + (list "--load" (format "%s.asd" system) + "--eval" (format "(require '%s)" system))) + ;; Heuristic: shorter names are earlier in the dependency tree. + ;; For example, X-test.asd depends on X.asd. + (sort (tw/find-asd-systems project-directory) + (lambda (s1 s2) + (< (length s1) (length s2))))) + ;; Assume the project directory name is the name of the main package. + "--eval" ,(format "(in-package #:%s)" + (file-name-base + (directory-file-name project-directory))))))) + +(use-package inf-lisp + :after (lisp-mode) + :hook (lisp-mode . tw/lisp-project-setup) + :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)) +(add-to-list 'safe-local-variable-values '(geiser-guile-binary . ("guix" "repl"))) +(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 -- cgit v1.2.3