Emacs Configuration
Early Init
;;; early-init.el -- Run at the beginning of Emacs startup sequence. -*- lexical-binding: t; buffer-read-only: t; -*-
;;; Commentary:
;; This file was generated by org-babel from config.org and should not be edited
;; directly.
;;; Code:Set an environment variable so children can detect that they’re inside Emacs
(setenv "INSIDE_EMACS" "true")Crank up the max size of subprocess output to read.
This is needed for language servers to perform well, since they communicate using large chunks of JSON.
(setq read-process-output-max (* 1024 1024))Make this checkout directory the value for user-emacs-directory
(setq user-emacs-directory (file-name-directory (or load-file-name (buffer-file-name))))Disable package.el to improve startup time
Disable package.el, since it increases startup time and I use
nix-community/emacs-overlay to handle package installation.
(setq package-enable-at-startup nil)Increase GC limit to reduce collections during startup
Garbage collection runs slow down the Emacs startup sequence. Temporarily increase settings. We will set final values at the end of the startup sequence.
(setq gc-cons-threshold most-positive-fixnum)
(setq gc-cons-percentage 0.7)Put the ELN cache directory in an XDG-conformant place
(when (boundp 'comp-eln-load-path)
(let ((cache-dir "~/.cache/emacs/eln-cache/"))
(mkdir cache-dir t)
(add-to-list 'comp-eln-load-path cache-dir)))Enable pixel-wise frame resizing
This helps tiling window managers apply the right size to Emacs frames.
(setq frame-resize-pixelwise t)Disable menu bars, etc
Disable window chrome that doesn’t make sense in a keyboard-driven UI.
(custom-set-variables '(menu-bar-mode nil)
'(tool-bar-mode . nil)
'(scroll-bar-mode nil))
(modify-all-frames-parameters '((vertical-scroll-bars)
(name . "Emacs")))File header
;;; config.el -- User init file for Emacs. -*- lexical-binding: t; buffer-read-only: t; -*-
;;; Commentary:
;; This file was generated by org-babel from config.org and should not be edited
;; directly.
;;; Code:Debugging
Hook into the startup lifecycle to report useful debugging information. Note
that these leverage the DEPTH so that subsequent functions added to
after-init-hook are still wrapped correctly.
Show profiler report if startup took too long
(require 'profiler)
(defvar startup-debugger-report-enabled-p t
"Whether to show the debugger if startup takes to long.
This can be disabled in the site file.")
(defvar startup-debugger-report-threshold-seconds 1)
(defun config-start-debugger-on-startup ()
(when startup-debugger-report-enabled-p
(profiler-start 'cpu)))
(defun config-stop-debugger-on-startup ()
(when startup-debugger-report-enabled-p
(profiler-stop)
(let* ((now (current-time))
(total-startup-time (float-time (time-subtract now emacs-start-time))))
(unless (time-less-p total-startup-time
(seconds-to-time startup-debugger-report-threshold-seconds))
(profiler-report)))))
(unless noninteractive
(add-hook 'after-init-hook #'config-start-debugger-on-startup -99)
(add-hook 'after-init-hook #'config-stop-debugger-on-startup 99))Enable debugging for duration of startup sequence
(setq debug-on-error t)
(add-hook 'after-init-hook
(lambda ()
(setq debug-on-error nil))
98)Log startup time duration
Note that tests refer to total-startup-duration, so it should not be inlined.
(defvar after-init-start-time)
(defvar total-startup-duration)
(add-hook 'after-init-hook (lambda ()
(setq after-init-start-time (current-time)))
-97)
(add-hook 'after-init-hook (lambda ()
(let* ((now (current-time))
(after-init-duration (float-time (time-subtract now after-init-start-time))))
(setq total-startup-duration (float-time (time-subtract now emacs-start-time)))
(message "after-init completed (%.3f hook duration, %.3f seconds total startup time)"
after-init-duration
total-startup-duration)))
97)Startup
Tune garbage collection
See: Why are you changing gc-cons-threshold?
(defvar config-default-gc-threshold 800000)
(defun config--inhibit-gc ()
(setq gc-cons-threshold most-positive-fixnum))
(defun config--enable-gc ()
(setq gc-cons-threshold config-default-gc-threshold))Restore GC settings after init sequence has completed
(add-hook 'after-init-hook #'config--enable-gc)Prevent GCs during user input in the minibuffer
(add-hook 'minibuffer-setup-hook #'config--inhibit-gc)
(add-hook 'minibuffer-exit-hook #'config--enable-gc)Start server
(unless noninteractive
(server-start))Define a function to select a value depending on the current OS theme
(defun gtk-theme-class ()
(with-temp-buffer
(ignore-errors
(call-process "gsettings" nil t nil
"get" "org.gnome.desktop.interface" "gtk-theme"))
(if (string-match-p "dark" (buffer-string))
'dark
'light)))
(defun macos-theme ()
(with-temp-buffer
(ignore-errors
(call-process "defaults" nil t nil
"read" "-g" "AppleInterfaceStyle"))
(if (string-match-p "dark" (buffer-string))
'dark
'light)))
(cl-defun choose-by-system-theme (&key light dark)
(let ((theme
(pcase system-type
('gnu/linux (gtk-theme-class))
('darwin (macos-theme)))))
(if (equal 'dark theme)
dark
light)))Set background colour based on OS theme
Set reasonable placeholder foreground and background colours until the theme is loaded. Use the current WM theme to determine whether to use light or dark colours.
(set-background-color (choose-by-system-theme :dark "#282c34" :light "#FDF6E3"))
(set-foreground-color (choose-by-system-theme :dark "#bbc2cf" :light "#556b72"))use-package - DSL for Lisp package configuration
See: jwiegley/use-package
(with-no-warnings
(setq use-package-always-defer t)
(setq use-package-minimum-reported-time 0.05)
(setq use-package-compute-statistics t)
(setq use-package-verbose (not noninteractive)))
(eval-when-compile
(require 'use-package))Define a helper function for loading files with use-package’s timing functionality.
(autoload 'use-package-require "use-package-core")
(defun load-file-with-stats (file)
(let ((name (intern (file-name-sans-extension (file-name-nondirectory file)))))
(eval
(macroexp-progn
(use-package-concat
(when use-package-compute-statistics
`((use-package-statistics-gather :config ',name nil)))
(use-package-require file)
(when use-package-compute-statistics
`((use-package-statistics-gather :config ',name nil))))))))general - Provides a rich key-binding DSL supported by use-package
See: noctuid/general.el
(use-package general
:demand t)delight - Change or hide minor-mode lighters
(use-package delight
:demand t)Load features used often in config
(require 'dash)
(require 'f)
(require 'subr-x)
(require 'seq)
(require 'seq-extras (expand-file-name "lisp/seq-extras.el" user-emacs-directory))Load cl early to avoid warnings caused by reorganised functions in Emacs 27+
(with-no-warnings
(require 'cl))Load autoloads
I slam all package autoloads into a single file and read them in here.
(load-file-with-stats (expand-file-name "config-autoloads.el" user-emacs-directory))Configure paths and config layout
(require 'paths (ignore-errors (expand-file-name "paths.el" user-emacs-directory)))
(paths-initialise)Configure no-littering to use these paths
Customises many packages to create a cleaner .emacs.d layout.
See: emacscollective/no-littering
(use-package no-littering
:demand t
:init
(setq no-littering-etc-directory paths-etc-directory)
(setq no-littering-var-directory paths-cache-directory))Teach recentf to use these paths
(use-package recentf
:after no-littering
:config
(add-to-list 'recentf-exclude no-littering-etc-directory)
(add-to-list 'recentf-exclude no-littering-var-directory))Load site settings
Load host-specific settings, which are not checked into version control.
(defconst user-site-file (expand-file-name "site.el" user-emacs-directory))
(when (file-exists-p user-site-file)
(load-file-with-stats user-site-file))Colour theme
doom-themes - Enable appropriate theme for OS theme
(use-package doom-themes
:demand t
:custom
(doom-themes-enable-bold t)
(doom-themes-enable-italic t)
:init
(add-to-list 'custom-theme-load-path (file-name-directory (locate-library "doom-themes")))
:config
(custom-theme-set-faces 'user
'(highlight ((t :inherit nil :foreground nil :background nil :bold t)))
'(org-drawer ((t :inherit org-special-keyword)))
'(org-roam-tag ((t :italic t)) t)
'(org-agenda-done ((t :inherit org-done :bold nil)) t)
'(org-link ((t :inherit link :bold nil)) t)
'(font-lock-keyword-face ((t :weight normal :bold nil)) t)
'(org-roam-link ((t :inherit org-link :underline nil)) t))
(with-eval-after-load 'doom-solarized-light-theme
(custom-theme-set-faces 'doom-solarized-light
'(mu4e-highlight-face ((t :foreground "#268bd2")) t)))
(with-eval-after-load 'doom-one-theme
(custom-theme-set-faces 'doom-one
'(mu4e-highlight-face ((t :foreground "#51afef")) t)))
(load-theme (choose-by-system-theme :light 'doom-solarized-light :dark 'doom-one) t))Define Lisp functions for switching theme via emacsclient
I have dark and light scripts I execute to change theme across all my
applications. The following functions will be invoked by those scripts over
emacsclient.
(defun config-themes-light ()
(dolist (theme custom-enabled-themes)
(disable-theme theme))
(load-theme 'doom-solarized-light t))
(defun config-themes-dark ()
(dolist (theme custom-enabled-themes)
(disable-theme theme))
(load-theme 'doom-one t))Common advice
(defun advice-ignore-errors (f &rest args)
(ignore-errors
(apply f args)))Definitions needed for config
Org-roam index
(defconst org-roam-index-node-id "0F0670F7-A280-4DD5-8FAC-1DB3D38CD37F")display-buffer variables
(defconst display-buffer-slot-diagnostics 1)
(defconst display-buffer-slot-repls 2)
(defconst display-buffer-slot-documents 3)Utility functions
(defun face-ancestors (face)
"List all faces that FACE transitively inherits from."
(let (result)
(while (and face (not (equal face 'unspecified)))
(setq result (cons face result))
(setq face (face-attribute face :inherit)))
(nreverse result)))(defun bounds-of-surrounding-lines (lines-before lines-after)
(let ((start
(save-excursion
(ignore-errors
(forward-line (- lines-before)))
(line-beginning-position)))
(end
(save-excursion
(ignore-errors
(forward-line lines-after))
(line-end-position))))
(list start end)))(defun display-buffer-fullframe (buffer alist)
(when-let* ((window (or (display-buffer-reuse-window buffer alist)
(display-buffer-same-window buffer alist)
(display-buffer-pop-up-window buffer alist)
(display-buffer-use-some-window buffer alist))))
(delete-other-windows window)
window))Customise builtin features
Set C source directory to use the source files from the Nix build
(use-package find-func
:custom
(find-function-C-source-directory (getenv "NIX_EMACS_SRC_DIR")))Always use one-char y-or-n-p
(defalias #'yes-or-no-p #'y-or-n-p)Don’t use the system trash can
(setq delete-by-moving-to-trash nil)Do not truncate the results of eval-expression
(setq eval-expression-print-length nil)
(setq eval-expression-print-level nil)Use ‘Emacs’, rather than the selected buffer, as the window manager’s title for frames
(setq frame-title-format "Emacs")Instantly display current keystrokes in mini buffer
(setq echo-keystrokes 0.02)Save cookies to a cache file.
(use-package url
:custom
(url-cookie-file (expand-file-name "cookies" paths-cache-directory)))Prefer more recent Lisp files to outdated ELC files when loading
(setq load-prefer-newer t)Automatically disconnect insecure connections
(use-package nsm
:custom
(nsm-noninteractive t))Disable file dialogs
(setq use-file-dialog nil)
(setq use-dialog-box nil)Enable useful commands that are disabled by default
(put 'narrow-to-region 'disabled nil)
(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)
(put 'erase-buffer 'disabled nil)Set global keybindings for toggle-debug-on-error and friends
(general-define-key "C-c e e" 'toggle-debug-on-error)
(general-define-key "C-c e q" 'toggle-debug-on-quit)General file formatting
Always insert a final newline, as per the Unix convention.
(setq require-final-newline t)Set reasonable default indentation settings
(setq-default fill-column 80)
(setq-default indent-tabs-mode nil)Make scripts executable after save
(add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p)Don’t require two spaces to signal the end of a sentence
I don’t use sentence-based commands that often anyway.
(setq sentence-end-double-space nil)Don’t nag when trying to create a new file or buffer
(setq confirm-nonexistent-file-or-buffer nil)Do not show ^M chars in files containing mixed UNIX and DOS line endings
(defun config--hide-dos-eol ()
(setq buffer-display-table (make-display-table))
(aset buffer-display-table ?\^M []))
(add-hook 'after-change-major-mode-hook #'config--hide-dos-eol)Use UTF-8 everywhere by default
(prefer-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-language-environment 'utf-8)Whitespace handling
Insert a leading space after comment start for new comment lines
(autoload 'thing-at-point-looking-at "thingatpt")
(defun config--comment-insert-space (&rest _)
(when (and comment-start
(thing-at-point-looking-at (regexp-quote comment-start)))
(unless (or (thing-at-point-looking-at (rx (+ space))))
(just-one-space))))
(advice-add #'comment-indent-new-line :after #'config--comment-insert-space)Bind cycle-spacing to M-SPC
(general-define-key "M-SPC" 'cycle-spacing)Disable unwanted features
Inhibit the default startup screen
(setq initial-scratch-message nil)
(setq inhibit-startup-message t)
(setq initial-major-mode 'fundamental-mode)Disable cursor blinking
(blink-cursor-mode -1)Never show the useless hello file
(defalias #'view-hello-file #'ignore)Unset 2-window scrolling shortcuts
(global-unset-key (kbd "<f2>"))
(global-unset-key (kbd "S-<f2>"))Disable audible bell
(setq ring-bell-function #'ignore)Don’t pollute directories with lockfiles
I only run one instance of Emacs and never need to prevent concurrent file access.
(setq create-lockfiles nil)Don’t nag when following symlinks to files under version control
(setq vc-follow-symlinks t)Don’t try to ping things that look like domain names
(use-package ffap
:custom
(ffap-machine-p-known 'reject))Disable warnings from obsolete advice system
These are caused by packages and are generally not actionable by me.
(setq ad-redefinition-action 'accept)Don’t confirm before killing subprocesses on exit
(setq confirm-kill-processes nil)
(defun config--suppress-no-process-prompt (fn &rest args)
(cl-labels ((process-list () nil))
(apply fn args)))
(advice-add #'save-buffers-kill-emacs :around #'config--suppress-no-process-prompt)Disable suspend-frame on C-z
(global-unset-key (kbd "C-z"))Convert ANSI color codes to text properties in shell output
(autoload 'ansi-color-apply-on-region "ansi-color")
(defun config--display-ansi-codes (buf &rest _)
(and (bufferp buf)
(string= (buffer-name buf) "*Shell Command Output*")
(with-current-buffer buf
(ansi-color-apply-on-region (point-min) (point-max)))))
(advice-add #'display-message-or-buffer :before #'config--display-ansi-codes)Minibuffer settings
Keep a longer history by default
(setq history-length 1000)Hide files with boring extensions from find-file
(defun config--ff-hide-boring-files-in-completion (result)
"Filter RESULT using `completion-ignored-extensions'."
(if (and (listp result) (stringp (car result)) (cdr result))
(let ((matches-boring (rx-to-string `(and (or "."
".."
".DS_Store"
"__pycache__/"
".cache/"
".ensime_cache/"
,@completion-ignored-extensions)
eos))))
(seq-remove (lambda (it)
(and (stringp it) (string-match-p matches-boring it)))
result))
result))
(advice-add #'completion--file-name-table :filter-return #'config--ff-hide-boring-files-in-completion)Remove lingering *completions* buffer whenever we exit the minibuffer
(defun config--cleanup-completions-buffer ()
(when-let* ((buf (get-buffer "*Completions*")))
(kill-buffer buf)))
(add-hook 'minibuffer-exit-hook #'config--cleanup-completions-buffer)Backup settings
Disable backup files
Meh, I use git.
(setq make-backup-files nil)
;; (setq kept-new-versions 6)
;; (setq delete-old-versions t)
;; (setq version-control t)Create autosave files inside the XDG cache directory.
(setq auto-save-file-name-transforms
`((".*" ,(expand-file-name "auto-save" paths-cache-directory) t)))Write custom settings to a separate file
Keep custom settings in a separate file. This keeps init.el clean.
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
(load custom-file nil t))Copy-paste & clipboard settings
Share the Emacs kill ring with the host OS clipboard
(setq select-enable-clipboard t)
(setq save-interprogram-paste-before-kill t)Prevent duplicated entries in the kill ring
(setq kill-do-not-save-duplicates t)Clean up whitespace when inserting yanked text
(defun config--yank-ws-cleanup (&rest _)
(whitespace-cleanup)
(delete-trailing-whitespace))
(advice-add #'insert-for-yank :after #'config--yank-ws-cleanup)Smooth scrolling
Anchor the cursor to the top or bottom of the window during scrolling, rather than paginating through the buffer.
(setq scroll-preserve-screen-position t)
(setq scroll-conservatively 101)comint - Base package for interpreter inferior processes
(use-package comint
:custom
(comint-prompt-read-only t))Help
Always focus on help windows
(setq help-window-select t)Don’t show ‘press q to close’ message
(advice-add 'help-window-display-message :override #'ignore)Customise how help buffers should be displayed
(add-to-list 'display-buffer-alist
`(,(rx bos "*Help*" eos)
(display-buffer-reuse-window display-buffer-pop-up-window)
(slot . ,display-buffer-slot-documents)
(reusable-frames . visible)
(side . right)
(window-width . 80)))apropos - searches for symbols matching a pattern
Extend apropos to search for more kinds of symbols.
(use-package apropos
:custom
(apropos-do-all t))saveplace - Persist the last location visited in a buffer
(use-package saveplace
:demand t
:config (save-place-mode +1))savehist - Save the minibuffer history across sessions
(use-package savehist
:demand t
:config (savehist-mode +1)
:custom
(savehist-additional-variables '(kill-ring
compile-command
search-ring
regexp-search-ring)))Bidirectional text editing
Configure Emacs so that each paragraph may have a difference text direction.
(setq-default bidi-paragraph-separate-re "^")
(setq-default bidi-paragraph-start-re "^")Prevent display-buffer from creating new frames
(defun config--display-buffer-fallback (buffer &rest _)
(when-let* ((win (split-window-sensibly)))
(with-selected-window win
(switch-to-buffer buffer)
(help-window-setup (selected-window))))
t)
(setq display-buffer-fallback-action
'((display-buffer--maybe-same-window
display-buffer-reuse-window
display-buffer-pop-up-window
display-buffer-in-previous-window
display-buffer-use-some-window
config--display-buffer-fallback)))Large file support
(defconst config--large-file-allowed-extensions
'("pdf" "png" "jpg" "jpeg"))
(defun config--dont-abort-if-allowed-extension (f &rest args)
(-let [(_size _op filename) args]
(unless (--any-p (f-ext-p filename it) config--large-file-allowed-extensions)
(apply f args))))
(advice-add #'abort-if-file-too-large :around #'config--dont-abort-if-allowed-extension)recentf - Recent files
(use-package recentf
:hook (after-init . recentf-mode)
:custom
(recentf-filename-handlers '(abbreviate-file-name))
(recentf-max-saved-items 100))Specify which files to exclude
(use-package recentf
:custom
(recentf-exclude '(config-recentf--boring-filename-p
config-recentf--boring-extension-p
file-remote-p
config-recentf--sudo-file-p
config-recentf--child-of-boring-relative-dir-p
config-recentf--child-of-boring-abs-dir-p))
:config
(defun config-recentf--boring-filename-p (f)
(memq (f-filename f) '("TAGS" ".DS_Store")))
(defun config-recentf--boring-extension-p (f)
(seq-intersection (f-ext f) '("gz" "zip" "tar")))
(defun config-recentf--sudo-file-p (f)
(string-prefix-p "/sudo:root@" f))
(defun config-recentf--child-of-boring-relative-dir-p (f)
(string-match-p (rx "/" (or ".g8" ".git" "Maildir" "build" "dist" "target" "vendor")
"/")
f))
(defconst config-recentf--abs-dirs
(seq-map (lambda (it) (f-slash (file-truename it)))
(list "/var/folders/"
"/usr/local/Cellar/"
"/tmp/"
"/nix/store/"
paths-cache-directory
paths-etc-directory)))
(defun config-recentf--child-of-boring-abs-dir-p (f)
(let ((ignore-case (eq system-type 'darwin)))
(seq-find (lambda (d)
(or
(string-prefix-p d f ignore-case)
(string-prefix-p d (file-truename f) ignore-case)))
config-recentf--abs-dirs))))Multilingual input support
Set up LaTeX-style input method and add extra MULE rules for common chars.
(use-package mule
:custom
(default-input-method "TeX")
:config
(defun config-mule--set-tex-method-vars ()
(when-let* ((quail-current-package (assoc "TeX" quail-package-alist)))
(quail-defrule ";" (quail-lookup-key "\\"))
(quail-define-rules ((append . t))
("\\null" ?∅)
("\\rarr" ?→)
("\\larr" ?←)
("\\lr" ?↔)
("\\lam" ?λ)
("\\Lam" ?Λ)
("\\all" ?∀)
("\\rtack" ?⊢))))
(add-hook 'input-method-activate-hook #'config-mule--set-tex-method-vars))autorevert - Revert buffers automatically if the file changes on disk
(use-package autorevert
:delight (auto-revert-mode " auto-revert")
:hook (after-init . global-auto-revert-mode)
:custom
(auto-revert-verbose nil))goto-addr - Turns URLs and mailto links into clickable buttons
(use-package goto-addr
:hook (prog-mode . goto-address-prog-mode))shr - Built-in HTML renderer
(use-package shr
:config
;; Undefine key that prevents forward-word in evil
(define-key shr-map (kbd "w") nil))hideshow - Basic code folding
Enable hideshow in all programming buffers
(use-package hideshow
:hook (prog-mode . hs-minor-mode))Use advice to ignore some boring errors
(use-package hideshow
:config
(advice-add 'hs-hide-all :around #'advice-ignore-errors)
(advice-add 'hs-hide-block :around 'advice-ignore-errors)
(advice-add 'hs-minor-mode :around #'advice-ignore-errors)
(advice-add 'hs-show-all :around #'advice-ignore-errors)
(advice-add 'hs-show-block :around #'advice-ignore-errors)
(advice-add 'hs-toggle-hiding :around #'advice-ignore-errors))authinfo - Store sensitive keys & passwords in an encrypted file
(use-package auth-source
:custom
(auth-sources '("~/.authinfo.gpg")))pixel-scroll - Enables pixel-wise scrolling
(use-package pixel-scroll
:demand t
:config (pixel-scroll-mode +1))Manuals
man - Manpages
(use-package man
:general (:keymaps 'Man-mode-map
"M-n" #'Man-next-section
"M-p" #'Man-previous-section))woman - system manual page reader
(use-package woman
:custom
(woman-fill-frame t)
(woman-default-indent 7))info - Info manual system
Emacs and many packages provide manuals in the info format. Configure this
system below.
(use-package info
:general
(:states 'normal :keymaps 'Info-mode-map
"C-n" 'Info-forward-node
"C-p" 'Info-backward-node))info+ - adds extra functionality to Info
(use-package info+
:after info
:demand t
:custom
(Info-fontify-angle-bracketed-flag nil))Image viewing
(use-package image
:general (:keymaps 'image-mode-map :states '(normal motion)
"-" #'image-decrease-size
"+" #'image-increase-size))compile - Mode for compilation buffers
(use-package compile
:custom
(compilation-environment '("TERM=screen-256color"))
(compilation-always-kill t)
(compilation-ask-about-save nil)
(compilation-scroll-output 'first-error))Colourise compilation output
(use-package compile
:config
(defun colourise-compilation-output ()
(let ((inhibit-read-only t))
(ansi-color-apply-on-region (save-excursion
(goto-char compilation-filter-start)
(line-beginning-position))
(point))))
(add-hook 'compilation-filter-hook 'colourise-compilation-output))Position compilation buffers
(use-package compile
:config
(add-to-list 'display-buffer-alist
`(,(rx bos "*compilation*" eos)
(display-buffer-reuse-window display-buffer-in-side-window)
(slot . ,display-buffer-slot-diagnostics)
(side . bottom)
(window-height . 0.4))))ediff - Interactive diff interface
Configure how ediff should display windows when started.
(use-package ediff
:custom
(ediff-window-setup-function #'ediff-setup-windows-plain)
(ediff-split-window-function #'split-window-horizontally))Teach ediff how to copy contents from both buffers in a three-way merge
(use-package ediff
:functions
(ediff-setup-windows-plain ediff-copy-diff ediff-get-region-contents)
:config
(defun ediff-copy-both-to-C ()
"Copy both ediff buffers in a 3-way merge to the target buffer."
(interactive)
(let ((str
(concat
(ediff-get-region-contents ediff-current-difference 'A ediff-control-buffer)
(ediff-get-region-contents ediff-current-difference 'B ediff-control-buffer))))
(ediff-copy-diff ediff-current-difference nil 'C nil str)))
(defun config-ediff--setup-keybinds ()
(define-key ediff-mode-map (kbd "B") #'ediff-copy-both-to-C))
(add-hook 'ediff-keymap-setup-hook #'config-ediff--setup-keybinds))Reveal the context around the selected hunk when diffing org buffers
(use-package ediff
:config
(autoload 'org-reveal "org")
(defun config-ediff--org-reveal-around-difference (&rest _)
(dolist (buf (list ediff-buffer-A ediff-buffer-B ediff-buffer-C))
(when (and buf (buffer-live-p buf))
(with-current-buffer buf
(when (derived-mode-p 'org-mode)
(org-reveal t))))))
(advice-add 'ediff-next-difference :after #'config-ediff--org-reveal-around-difference)
(advice-add 'ediff-previous-difference :after #'config-ediff--org-reveal-around-difference))world-time-mode - World clock UI
(use-package world-time-mode
:general
(:states 'normal :keymaps 'world-time-table-mode-map "q" 'quit-window)
:custom
(display-time-world-list '(("Pacific/Auckland" "NZT")
("America/Los_Angeles" "Pacific Time")
("Europe/Istanbul" "Turkey")
("Asia/Beirut" "Lebanon")
("Europe/Berlin" "Euro Central")
("UTC" "UTC")))
:config
(add-hook 'world-time-table-mode-hook 'hl-line-mode))eldoc - Show documentation in the minibuffer
(use-package eldoc
:hook (emacs-lisp-mode . eldoc-mode)
:custom
(eldoc-idle-delay 0.2))Suppress eldoc when point is at a flycheck error
M-n to end in completing-read will use the thing at point
(autoload 'ffap-guesser "ffap")
(defun config--minibuffer-default-add-function ()
(with-selected-window (minibuffer-selected-window)
(delete-dups
(delq nil
(list (thing-at-point 'symbol)
(thing-at-point 'list)
(ffap-guesser)
(thing-at-point-url-at-point))))))
(setq minibuffer-default-add-function #'config--minibuffer-default-add-function)Better eval-expression
Define an alternative version of eval-expression that uses emacs-lisp-mode to
provide font-locking, and handles smartparens better.
See: Re: How properly utilize the minibuffer and inactive minibuffer startup
(defvar eval-expression-interactively-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map read-expression-map)
(define-key map (kbd "<escape>") #'abort-minibuffers)
(define-key map (kbd "C-g") #'abort-minibuffers)
map))
(defun eval-expression-interactively--read (prompt &optional initial-contents)
(let ((minibuffer-completing-symbol t))
(minibuffer-with-setup-hook
(lambda ()
(let ((inhibit-message t))
(emacs-lisp-mode)
(use-local-map eval-expression-interactively-map)
(setq font-lock-mode t)
(funcall font-lock-function 1)))
(read-from-minibuffer prompt initial-contents
eval-expression-interactively-map nil
'read-expression-history))))
(autoload 'pp-display-expression "pp")
(autoload 'pp-to-string "pp")
(defun eval-expression-interactively (expression &optional arg)
"Like `eval-expression' with nicer input handling.
- Use `emacs-lisp-mode' to provide font locking and better
integration with other packages.
- Use the `pp' library to display the output in a readable form.
EXPRESSION is a Lisp form to evaluate.
With optional prefix ARG, insert the results into the buffer at
point."
(interactive (list (read (eval-expression-interactively--read "Eval: "))
current-prefix-arg))
(if arg
(insert (pp-to-string (eval expression lexical-binding)))
(pp-display-expression (eval expression lexical-binding)
"*Pp Eval Output*")))Bind this command to M-:
(general-define-key :keymaps 'override :states '(normal motion visual)
"M-:" 'eval-expression-interactively)Use this command for evaluating expressions in the Lisp debugger too
(use-package debug
:config
(advice-add 'debugger-record-expression
:around
(lambda (f exp)
(interactive (list (read (eval-expression-interactively--read "Eval: "))))
(funcall f exp))
'((name . use-eval-expression-interactively--read))))Improve basic editing configuration for all modes
Use control key to transpose lines up and down
(autoload 'org-move-item-down "org-list")
(autoload 'org-move-item-up "org-list")
(defun transpose-line-up ()
"Move the current line up."
(interactive)
(if (derived-mode-p 'org-mode)
(org-move-item-up)
(transpose-lines 1)
(forward-line -2)
(indent-according-to-mode)))
(defun transpose-line-down ()
"Move the current line up."
(interactive)
(if (derived-mode-p 'org-mode)
(org-move-item-down)
(forward-line 1)
(transpose-lines 1)
(forward-line -1)
(indent-according-to-mode)))
(global-set-key (kbd "C-<up>") #'transpose-line-up)
(global-set-key (kbd "C-<down>") #'transpose-line-down)Useful interactive functions
(defun insert-uuid ()
"Insert a UUID at point."
(interactive "*")
(insert (string-trim (shell-command-to-string "uuidgen"))))(defun insert-date (str)
"Read date string STR interactively and insert it at point."
(interactive (list
(if (not current-prefix-arg)
(format-time-string "%F")
(let ((formats (seq-map #'format-time-string
'("%F"
"%F %R"
"%X"
"%c"))))
(completing-read "Format: " formats nil t)))))
(insert str))Define a command for reversing the characters isrc
Define a command to indent every line in the buffer
This should really be a thing out-of-the-box.
(defun indent-buffer ()
"Indent the entire buffer."
(interactive "*")
(save-excursion
(delete-trailing-whitespace)
(indent-region (point-min) (point-max) nil)
(untabify (point-min) (point-max))))Define a command to perform indentation in a context-sensitive way
(autoload 'lsp-format-region "lsp-mode")
(autoload 'lsp-format-buffer "lsp-mode")
(defun config-indent-dwim (&optional justify)
"Indent the thing at point.
Knows how to fill strings and comments, or indent code.
Optional arg JUSTIFY will justify comments and strings."
(interactive "*P")
(-let [(_ _ _ string-p comment-p) (syntax-ppss)]
(cond
(string-p
(let ((progress (make-progress-reporter "Filling paragraph")))
(fill-paragraph justify)
(progress-reporter-done progress)))
(comment-p
(let ((progress (make-progress-reporter "Filling comment")))
(fill-comment-paragraph justify)
(progress-reporter-done progress)))
((region-active-p)
(cond
((bound-and-true-p lsp-mode)
(lsp-format-region (region-beginning) (region-end)))
(t
(indent-region (region-beginning) (region-end)))))
(t
(let ((progress (make-progress-reporter "Indenting buffer")))
(cond
((bound-and-true-p format-all-mode)
(format-all-buffer))
((bound-and-true-p lsp-mode)
(lsp-format-buffer))
(t
(indent-buffer)))
(progress-reporter-done progress))))))
(define-key prog-mode-map (kbd "M-q") #'config-indent-dwim)ws-butler - Automatic whitespace cleanup while editing
(use-package ws-butler
:hook
(prog-mode . ws-butler-mode)
(text-mode . ws-butler-mode))unfill - Paragraph fill/unfill
unfill provides a command that is the opposite of fill-paragraph.
(use-package unfill
:commands (unfill-region unfill-paragraph unfill-toggle))align - Provides useful functions for aligning text
(use-package align
:general ("C-x a a" #'align-regexp))hide-comnt - Toggle whether comments are visible
(use-package hide-comnt
:commands (hide/show-comments-toggle))dumb-jump - Generic jump-to-definition support
dump-jump provides a good fallback for navigating to definitions in the absence
of an LSP or semantic analysis.
(use-package dumb-jump
:general (:states 'normal :keymaps 'prog-mode-map "M-." #'jump-to-definition)
:custom
(dumb-jump-selector 'completing-read))auto-insert - File templates
autoinsert provides file templates.
(use-package autoinsert
:preface
(defvar auto-insert-alist nil)
:hook (find-file . auto-insert)
:custom
(auto-insert-query nil))Extend auto-insert to use the more intuitive yasnippet DSL.
(use-package autoinsert-files
:after (autoinsert)
:demand t
:commands (autoinsert-files-populate-templates)
:init
(defun autoinsert-maybe-enter-snippet-mode ()
(require 'autoinsert)
(when (string-prefix-p auto-insert-directory (buffer-file-name))
(snippet-mode)))
(add-hook 'find-file-hook #'autoinsert-maybe-enter-snippet-mode)
:config
(advice-add 'auto-insert :before (lambda (&rest _)
(autoinsert-files-populate-templates))))ispell - Spellchecking commands
(use-package ispell
:commands (ispell-check-version ispell-find-aspell-dictionaries)
:custom
(ispell-program-name "aspell")
(ispell-dictionary "en_GB")
(ispell-silently-savep t)
:config
(ispell-check-version)
(setq ispell-dictionary-alist (ispell-find-aspell-dictionaries)))flyspell - Incremental spellchecking
(use-package flyspell
:hook
(org-mode . flyspell-mode)
:custom
(flyspell-issue-welcome-flag nil)
(flyspell-default-dictionary "en_GB"))Prevent flyspell from showing suggestions in more contexts
(use-package flyspell
:after (org)
:config
(defun flyspell-on-org-verify (result)
(and result
(not (seq-intersection (face-at-point nil t)
'(org-link verb-header)))))
(advice-add 'org-mode-flyspell-verify :filter-return #'flyspell-on-org-verify))undo-tree - Visual graph for undo history
(use-package undo-tree
:hook (org-mode . undo-tree-mode)
:general
("C-x t" 'undo-tree-visualize)
(:states 'normal :keymaps 'org-mode-map
"C-r" 'undo-tree-redo
"u" 'undo-tree-undo))format-all - Generic format-on-save system
(use-package format-all
:hook
(typescript-mode . format-all-mode)
(typescript-mode . format-all-ensure-formatter)
(nix-mode . format-all-mode)
(nix-mode . format-all-ensure-formatter)
(terraform-mode . format-all-mode)
(terraform-mode . format-all-ensure-formatter)
:custom
(format-all-show-errors 'never))emojify - Render emoji
(use-package emojify
:hook (after-init . global-emojify-mode)
:custom
(emojify-emoji-styles '(github unicode))
(emojify-program-contexts '(comments))
(emojify-point-entered-behaviour 'uncover)
(emojify-user-emojis
'((":check:" . (("emoji" . ":white_check_mark:")
("name" . "White Heavy Check Mark")
("unicode" . "✅")
("image" . "2705.png")
("style" . "github")))))
:config
(defun emojify-at-org-drawer-p (&rest _)
(when (derived-mode-p 'org-mode 'org-agenda-mode)
(save-excursion
(goto-char (line-beginning-position))
(or (org-at-drawer-p) (org-at-property-p)))))
(add-to-list 'emojify-inhibit-functions #'emojify-at-org-drawer-p))yasnippet - Text snippets
yasnippet provides expandable text snippets. I use them extensively to cut
down on typing.
(use-package yasnippet
:hook
(prog-mode . (lambda () (require 'yasnippet)))
(text-mode . (lambda () (require 'yasnippet)))
:custom
(yas-wrap-around-region t)
(yas-alias-to-yas/prefix-p nil)
(yas-prompt-functions '(yas-completing-prompt))
(yas-verbosity 0)
(yas-minor-mode-map (make-sparse-keymap))
:general
(:keymaps 'yas-minor-mode-map :states 'insert
"TAB"
(general-predicate-dispatch 'indent-for-tab-command
(yas-maybe-expand-abbrev-key-filter t) 'yas-expand))
(:keymaps 'yas-keymap :states 'insert
"SPC"
(general-predicate-dispatch 'self-insert-command
(yas--maybe-clear-field-filter t) 'yas-skip-and-clear-field)
"<backspace>"
(general-predicate-dispatch 'backward-delete-char
(yas--maybe-clear-field-filter t) 'yas-skip-and-clear-field
(bound-and-true-p smartparens-mode) 'sp-backward-delete-char))
:config
(yas-global-mode +1))Customise backwards cycling behaviour
When cycling backward through fields, place point at the end of the previous field.
(use-package yasnippet
:config
(defun config-yasnippet--end-of-field ()
(when-let* ((field (yas-current-field)))
(marker-position (yas--field-end field))))
(defun config-yasnippet--maybe-goto-field-end ()
"Move to the end of the current field if it has been modified."
(when-let* ((field (yas-current-field)))
(when (and (yas--field-modified-p field)
(yas--field-contains-point-p field))
(goto-char (config-yasnippet--end-of-field)))))
(defun yasnippet-goto-field-end (&rest _)
(config-yasnippet--maybe-goto-field-end)
(when (and (boundp 'evil-mode) evil-mode (fboundp 'evil-insert-state))
(evil-insert-state)))
(advice-add 'yas-next-field :after #'yasnippet-goto-field-end)
(advice-add 'yas-prev-field :after #'yasnippet-goto-field-end))Snippet functions
These functions are used in the definitions of snippets.
General
(defun yas-funcs-bolp ()
"Non-nil if point is on an empty line or at the first word.
The rest of the line must be blank."
(let ((line (buffer-substring (line-beginning-position) (line-end-position))))
(string-match-p (rx bol (* space) (* word) (* space) eol)
line)))emacs-lisp
(defun yas-funcs-el-custom-group ()
"Find the first group defined in the current file.
Fall back to the file name sans extension."
(or
(cadr (s-match (rx "(defgroup" (+ space) (group (+ (not space))))
(buffer-string)))
(cadr (s-match (rx ":group" (+ space) "'" (group (+ (any "-" alnum))))
(buffer-string)))
(file-name-sans-extension (file-name-nondirectory buffer-file-name))))
(defun yas-funcs-el-autoload-file (sym)
(if-let* ((file (symbol-file (if (stringp sym) (intern sym) sym))))
(file-name-sans-extension (file-name-nondirectory file))
""))
(defun yas-funcs-el-at-line-above-decl-p ()
(save-excursion
(forward-line)
(back-to-indentation)
(thing-at-point-looking-at (rx (* space) "("
(or "cl-defun" "defun" "defvar" "defconst"
"define-minor-mode"
"define-globalized-minor-mode"
"define-derived-mode")))))
(defun yas-funcs-el-package-prefix ()
(cond
((string-prefix-p "*Org Src" (buffer-name))
"")
((bound-and-true-p nameless-current-name)
(format "%s-" nameless-current-name))
(t
(format "%s-" (f-base (or (buffer-file-name) (buffer-name)))))))
(defun yas-funcs-buttercup-file-p ()
(string-match-p "^test-" (file-name-nondirectory (buffer-file-name))))TypeScript/JavaScript
(defcustom yas-funcs-js-import-to-module-alist '()
"Map the name of a default import to a module.
Expected to be set via directory variable."
:type '(alist :key-type string :value-type string)
:group 'yas-funcs
:safe (lambda (it)
(and (listp it)
(seq-every-p #'car #'stringp)
(seq-every-p #'cdr #'stringp))))
(use-package yasnippet
:config
(cl-defun yas-funcs-js-module-name-for-binding (&optional (text yas-text))
(pcase text
('nil "")
("" "")
((guard (assoc (string-trim text) yas-funcs-js-import-to-module-alist))
(cdr (assoc (string-trim text) yas-funcs-js-import-to-module-alist)))
("VError" "verror")
("memoize" "promise-memoize")
((or "aws" "AWS") "aws-sdk")
("_" "lodash")
("schema" "@broca/schema")
("loadConfiguration" "@broca/config")
("logger" "@broca/logger")
("* as GQL" "@broca/gql")
((guard (s-contains? "{" text))
"")
(s
(-if-let* ((match-binding (rx (* space) "*" (+ space) "as" (+ space) (group (+ (not (any space))))))
((_ name) (s-match match-binding text)))
(yas-funcs-js-module-name-for-binding name)
(s-downcase (s-dashed-words s))))))
(defun yas-funcs-js-ctor-body (argstring)
(when argstring
(thread-last argstring
(s-split (rx (or "," ".")))
(-map #'s-trim)
(-remove #'s-blank?)
(--map (format "this.%s = %s;" it it))
(s-join "\n"))))
(defun yas-funcs-js-buffer-imports-logger-p ()
(let ((str (buffer-substring-no-properties (point-min) (point-max))))
(string-match-p (rx bol "import" (+ space) symbol-start "logger" symbol-end) str)))
(defun yas-funcs-js-inside-describe-p ()
(save-excursion
(search-backward-regexp (rx bol (* space) symbol-start "describe" symbol-end) nil t))))editorconfig - Support editorconfig files
(use-package editorconfig
:hook (after-init . editorconfig-mode))direnv - Support direnv files
Teach Emacs how to load environment variables from direnv.
(use-package direnv
:hook (after-init . direnv-mode)
:custom
(direnv-always-show-summary nil))rainbow-mode - Apply colours to hex strings in buffers
(use-package rainbow-mode
:hook
(help-mode . rainbow-mode)
(emacs-lisp-mode . rainbow-mode)
(css-mode . rainbow-mode))evil - Vim-style modal editing
evil provides macros that I want to use in :config blocks, so teach the
byte-compiler about them to avoid warnings.
(cl-eval-when (compile)
(require 'evil))Customise global vars and keybindings
(autoload 'man-completing "man-completing")
(use-package evil
:hook (after-init . evil-mode)
:custom
(evil-lookup-func #'man-completing)
(evil-mode-line-format nil)
(evil-shift-width 2)
(evil-undo-system 'undo-redo)
(evil-symbol-word-search t)
(evil-want-visual-char-semi-exclusive t)
(evil-want-Y-yank-to-eol t)
(evil-motion-state-cursor '("plum3" box))
(evil-visual-state-cursor '("gray" hbar))
(evil-normal-state-cursor '("IndianRed" box))
(evil-insert-state-cursor '("chartreuse3" bar))
(evil-emacs-state-cursor '("SkyBlue2" (box . t)))
:general
(:states 'normal "go" #'browse-url-at-point))Prevent visual state from updating the clipboard
(advice-add 'evil-visual-update-x-selection :override #'ignore)Prevent evil’s own keybindings from loading
We use evil-collection to manage these instead.
(use-package evil
:custom
(evil-want-keybinding nil)
(evil-want-integration t))Execute macro bound to q with Q
Use Q in normal state to execute the macro bound to q register. This is a
convenient way to quickly define a macro, then execute it immediately–just
double-tap q to record, then hit Q to execute.
(use-package evil
:general (:states 'normal "Q" #'config-evil--execute-Q-macro)
:preface
(defun config-evil--execute-Q-macro (count)
"Execute the macro bound to the Q register.
COUNT is the number of repetitions."
(interactive (list
(if current-prefix-arg
(if (numberp current-prefix-arg) current-prefix-arg 0)
1)))
(evil-execute-macro count (evil-get-register ?Q t))))Invert motions in RTL languages
Make motions make more sense by following RTL text direction in Arabic, Farsi etc.
(use-package evil-bidi
:after (evil)
:demand t)Customise navigation in help buffers
(use-package evil
:general
(:states 'motion :keymaps 'help-mode-map
"<escape>" 'quit-window
"^" 'help-go-back
"gh" 'help-follow-symbol))Customise initial states of different modes
(use-package evil
:config
(evil-set-initial-state 'anaconda-mode-view-mode 'motion)
(evil-set-initial-state 'diff-mode 'motion)
(evil-set-initial-state 'ert-simple-view-mode 'motion)
(evil-set-initial-state 'eshell-mode 'insert)
(evil-set-initial-state 'flycheck-error-list-mode 'motion)
(evil-set-initial-state 'grep-mode 'normal)
(evil-set-initial-state 'haskell-debug-mode 'motion)
(evil-set-initial-state 'helpful-mode 'motion)
(evil-set-initial-state 'ibuffer-mode 'motion)
(evil-set-initial-state 'nix-repl-mode 'insert)
(evil-set-initial-state 'occur-mode 'normal)
(evil-set-initial-state 'org-agenda-mode 'motion)
(evil-set-initial-state 'prodigy-mode 'motion)
(evil-set-initial-state 'profiler-report-mode 'motion)
(evil-set-initial-state 'racer-help-mode 'motion)
(evil-set-initial-state 'tabulated-list-mode 'motion)
(evil-set-initial-state 'vterm-mode 'emacs)
(evil-set-initial-state 'wdired-mode 'normal)
(with-eval-after-load 'replace
(evil-add-hjkl-bindings occur-mode-map)))Archive navigation integration
(use-package evil
:after (tar-mode)
:config
(evil-set-initial-state 'tar-mode 'emacs)
(evil-add-hjkl-bindings tar-mode-map))(use-package evil
:after (arc-mode)
:general
(:states 'motion :keymaps 'archive-mode-map
"q" 'kill-this-buffer
"o" 'archive-extract-other-window
"m" 'archive-mark
"x" 'archive-expunge
"U" 'archive-unmark-all-files
"j" 'archive-next-line
"k" 'archive-previous-line
"<return>" 'archive-extract)
:config
(evil-set-initial-state 'archive-mode 'emacs))compilation integration
Disable h (help) binding in compilation-mode, which interferes with evil
navigation.
(use-package evil
:general (:states 'motion :keymaps 'compilation-mode-map
"h" #'evil-backward-char))hydra integration
evil breaks cursor settings when combined with hydra. To work around this, never
show the cursor in deselected windows.
(setq-default cursor-in-non-selected-windows nil)Spellchecker integration
Add vim-style :spell and :nospell ex commands
(use-package evil
:config
(defun evil-flyspell-on ()
"Enable flyspell."
(interactive)
(turn-on-flyspell))
(defun evil-flyspell-off ()
"Disable flyspell."
(interactive)
(turn-off-flyspell))
(evil-ex-define-cmd "nospell" #'evil-flyspell-off)
(evil-ex-define-cmd "spell" #'evil-flyspell-on))Add more key bindings to work with spell-checker from normal state
(use-package evil-ispell
:after evil
:general (:states 'normal
"z SPC" #'flyspell-auto-correct-word
"zU" #'evil-ispell-correct-word
"zg" #'evil-ispell-mark-word-as-good
"zG" #'evil-ispell-mark-word-as-locally-good
"zn" #'evil-ispell-next-spelling-error
"zp" #'evil-ispell-previous-spelling-error))Use escape key as keyboard-quit
(general-define-key :keymaps '(minibuffer-local-map
minibuffer-local-ns-map
minibuffer-local-completion-map
minibuffer-local-must-match-map
minibuffer-local-isearch-map)
"<escape>" 'keyboard-escape-quit)link-hint - Teach evil how to navigate using links in org buffers and the agenda
(use-package link-hint
:after (evil)
:config
(put 'link-hint-org-link :vars '(org-mode org-agenda-mode)))evil-surround - Teach evil how to wrap objects with matched pairs
(use-package evil-surround
:after (evil)
:demand t
:config (global-evil-surround-mode +1)
:general
(:states 'visual :keymaps 'evil-surround-mode-map
"s" #'evil-surround-region
"S" #'evil-substitute)
:custom
(evil-surround-pairs-alist '((?\( . ("(" . ")"))
(?\[ . ("[" . "]"))
(?\{ . ("{" . "}"))
(?\) . ("(" . ")"))
(?\] . ("[" . "]"))
(?\} . ("{" . "}"))
(?# . ("#{" . "}"))
(?b . ("(" . ")"))
(?B . ("{" . "}"))
(?> . ("<" . ">"))
(?t . evil-surround-read-tag)
(?< . evil-surround-read-tag)
(?f . evil-surround-function))))Define an extra `sym'= pair for =emacs-lisp-mode
(use-package evil-surround
:after (evil)
:preface
(defun config-evil--init-evil-surround-pairs ()
(make-local-variable 'evil-surround-pairs-alist)
(push '(?\` . ("`" . "'")) evil-surround-pairs-alist))
:hook
(emacs-lisp-mode-hook . config-evil--init-evil-surround-pairs))evil-collection - Community-maintained bindings
(use-package evil-collection
:after (evil)
:demand t
:config
(evil-collection-init))evil-args - Text motions for function parameter lists
(use-package evil-args
:after (evil)
:general (:keymaps
'evil-inner-text-objects-map "a" #'evil-inner-arg
:keymaps
'evil-outer-text-objects-map "a" #'evil-outer-arg))evil-matchit - Teach % how to match more kinds of pairs
(use-package evil-matchit
:after (evil)
:demand t
:config
(global-evil-matchit-mode +1))evil-numbers - Use + and - to change number at point
(use-package evil-numbers
:after (evil)
:demand t
:general (:states 'normal
"+" #'evil-numbers/inc-at-pt
"-" #'evil-numbers/dec-at-pt))Teach < and > to shift text in a context-sensitive way
(use-package evil
:general (:states 'visual
"<" #'config-evil--shift-left
">" #'config-evil--shift-right)
:preface
(defun config-evil--shift-left (&optional beg end)
"Shift left, keeping the region active.
BEG and END are the bounds of the active region."
(interactive "r")
(evil-shift-left beg end)
(evil-normal-state)
(evil-visual-restore))
(defun config-evil--shift-right (&optional beg end)
"Shift right, keeping the region active.
BEG and END are the bounds of the active region."
(interactive "r")
(evil-shift-right beg end)
(evil-normal-state)
(evil-visual-restore)))evil-iedit-state - Easy renaming of symbol at point
iedit adds useful mass-renaming functionality. This package provides evil
compatibility.
(use-package evil-iedit-state
:commands (evil-iedit-state/iedit-mode))HACK: work around missing function
https://github.com/syl20bnr/evil-iedit-state/issues/36
(defalias 'evil-redirect-digit-argument #'ignore)lsp-mode - language-server support
Inject language servers into exec-path on Darwin
macOS prevents PATH being modified for graphical apps, so the wrapper set up with Nix won’t work. Manually add language servers to the path here.
(when (equal system-type 'darwin)
(dolist (dir (split-string (getenv "NIX_EMACS_DARWIN_PATH_EXTRAS") ":"))
(push dir exec-path))
(setq exec-path (seq-uniq exec-path))
(setenv "PATH" (string-join exec-path ":")))Set which modes use LSP
(defconst lsp-enabled-modes
'(graphql-mode
js-mode
json-mode
nix-mode
sh-mode
terraform-mode
typescript-mode
yaml-mode))
(dolist (mode lsp-enabled-modes)
(add-hook (intern (format "%s-hook" mode))
'lsp-deferred))Configure lsp-mode variables
(use-package lsp-mode
:commands (lsp lsp-deferred)
:general
(:keymaps 'lsp-mode-map
"C-c C-c" 'lsp-execute-code-action
[remap evil-lookup] 'lsp-describe-thing-at-point
[remap jump-to-definition] 'lsp-find-definition)
:custom
(lsp-headerline-breadcrumb-enable nil)
(lsp-modeline-code-actions-enable nil)
(lsp-modeline-diagnostics-enable nil)
(lsp-modeline-workspace-status-enable nil)
(lsp-auto-execute-action nil)
(lsp-enable-on-type-formatting nil)
(lsp-restart 'auto-restart)
(lsp-clients-typescript-tls-path (getenv "NIX_EMACS_TS_LANGUAGE_SERVER"))
(lsp-groovy-server-file (getenv "NIX_EMACS_GROOVY_LANGUAGE_SERVER_JAR"))
(lsp-eslint-validate '("typescript" "javascript" "javascriptreact"))
(lsp-eslint-server-command (list (getenv "NIX_EMACS_LSP_ESLINT_NODE_PATH")
(getenv "NIX_EMACS_ESLINT_SERVER_SCRIPT")
"--stdio")))lsp-ui - Additional package providing richer UI elements for lsp-mode
(use-package lsp-ui
:after lsp-mode
:general
(:keymaps 'lsp-ui-mode-map
[remap lsp-describe-thing-at-point] 'lsp-ui-show
[remap imenu] 'lsp-ui-imenu
[remap consult-imenu] 'lsp-ui-imenu)
:custom
(lsp-ui-imenu-auto-refresh 'after-save)
(lsp-ui-doc-enable nil)
:hook
(json-mode . lsp-ui-doc-mode)
(yaml-mode . lsp-ui-doc-mode))Set up leader keys
Note that we ensure evil is loaded first before binding any keys below,
otherwise general is pathologically slow.
See:
Use SPC as the global leader key
(use-package general
:after evil
:demand t
:config
(general-define-key :states '(normal motion) "SPC" nil))
(defmacro leader-set-key (&rest args)
(declare (indent defun))
`(use-package general
:after evil
:demand t
:config
(,'general-def ,@args ,@'(:keymaps 'override :states
'(normal motion visual)
:prefix "SPC"))))Top-level leader keybindings
(defun alternate-buffer (&optional window)
"Toggle back and forth between two buffers.
WINDOW sets the window in which to toggle, and defaults to the
current window."
(interactive)
(let ((current-buffer (window-buffer window))
(buffer-predicate (frame-parameter (window-frame window) 'buffer-predicate)))
;; switch to first buffer previously shown in this window that matches
;; frame-parameter `buffer-predicate'
(switch-to-buffer
(or (car (seq-filter (lambda (buffer)
(and (not (eq buffer current-buffer))
(or (null buffer-predicate) (funcall buffer-predicate buffer))))
(seq-map #'car (window-prev-buffers window))))
;; `other-buffer' honors `buffer-predicate' so no need to filter
(other-buffer current-buffer t)))))(leader-set-key
"$" '(popper-toggle-latest :wk "toggle popups")
"+" '(popper-toggle-type :wk "toggle popup or normal")
"-" '(popper-kill-latest-popup :wk "kill latest popup")
"!" '(async-shell-command :wk "shell cmd (async)")
"'" (general-predicate-dispatch 'poporg-dwim
(bound-and-true-p poporg-mode) 'poporg-edit-exit
(bound-and-true-p edit-indirect--overlay) 'edit-indirect-commit
(equal (buffer-name) "*Edit Formulas*") 'org-table-fedit-finish
(derived-mode-p 'org-mode) 'org-edit-special
(and (derived-mode-p 'markdown-mode) (markdown-code-block-at-point-p)) 'markdown-edit-code-block
(bound-and-true-p org-src-mode) 'org-edit-src-exit)
"/" '(consult-ripgrep :wk "rg")
":" '(eval-expression-interactively :wk "eval")
"<tab>" (list (general-predicate-dispatch 'alternate-buffer
(and (bound-and-true-p popper-popup-status) (memq popper-popup-status '(popup user-popup))) 'popper-cycle)
:wk "other buf")
"?" '(general-describe-keybindings :wk "show bindings")
"@" '(consult-bookmark :wk "bookmark")
"|" '(rotate-layout :wk "rotate window layout")
"SPC" '(consult-buffer :wk "switch buf")
"C" #'compile
"D" '(dired-other-window :wk "dired (other)")
"S" '(deadgrep :wk "rg (deadgrep)")
"d" #'dired
"i" (list (general-predicate-dispatch 'consult-imenu
(bound-and-true-p lsp-ui-mode) 'lsp-ui-imenu
(derived-mode-p 'lsp-ui-imenu-mode) 'lsp-ui-imenu--kill)
:wk "imenu")
"q" '(delete-window :wk "delete window")
"r" 'selectrum-repeat
"s" '(evil-iedit-state/iedit-mode :wk "iedit")
"u" '(universal-argument :wk "prefix arg")
"x" '(execute-extended-command :wk "M-x"))~,~ - Parens
(leader-set-key :infix ","
"" '(nil :wk "parens")
"h" '(sp-beginning-of-sexp :wk "go to start")
"l" '(sp-end-of-sexp :wk "go to end")
"n" '(sp-next-sexp :wk "next")
"p" '(sp-previous-sexp :wk "prev")
"<" '(sp-backward-up-sexp :wk "backward up")
">" '(sp-up-sexp :wk "up")
"c" '(sp-convolute-sexp :wk "convolute")
"d" '(sp-kill-sexp :wk "kill")
"D" '(sp-backward-kill-sexp :wk "kill backward")
"k" '(sp-splice-sexp-killing-forward :wk "splice (forward)")
"K" '(sp-splice-sexp-killing-backward :wk "splice (back)")
"s" '(sp-splice-sexp-killing-around :wk "splice (around)")
"r" '(sp-raise-sexp :wk "raise")
"a" '(sp-add-to-next-sexp :wk "add to next")
"A" '(sp-add-to-previous-sexp :wk "add to prev")
"b" '(sp-forward-barf-sexp :wk "barf (forward)")
"B" '(sp-backward-barf-sexp :wk "barf (back)")
"m" '(sp-forward-slurp-sexp :wk "slurp (forward)")
"M" '(sp-backward-slurp-sexp :wk "slurp (back)")
"e" '(sp-emit-sexp :wk "emit")
"j" '(sp-join-sexp :wk "joi")
"t" '(sp-transpose-sexp :wk "transpose")
"U" '(sp-backward-unwrap-sexp :wk "unwrap (back)")
"u" '(sp-unwrap-sexp :wk "unwrap (forward)")
"w" '(sp-rewrap-sexp :wk "rewrap")
"x" '(sp-split-sexp :wk "split")
"y" '(sp-copy-sexp :wk "copy (forward)")
"Y" '(sp-backward-copy-sexp :wk "copy (back)"))a - Applications
(defun profiler-stop-and-report (&optional continue-p)
"Stop the profiler and show results.
With optional prefix arg CONTINUE-P, keep profiling."
(interactive "P")
(let ((ran-p (profiler-running-p)))
(unless continue-p
(profiler-stop))
(profiler-report)
(when ran-p
(if continue-p
(message "Profiler still recording")
(message "Profiler stopped")))))
(leader-set-key :infix "a"
"" '(nil :wk "apps")
"c" #'quick-calc
"C" #'full-calc
"e" #'eshell
"m" #'mu4e
"p" #'pass
"r" (general-predicate-dispatch 'profiler-start
(and (featurep 'profiler) (profiler-running-p)) 'profiler-stop-and-report)
"w" #'world-time-list)b - Buffers
(leader-set-key :infix "b"
"" '(nil :wk "bufs")
"n" '(next-buffer :wk "next")
"p" '(previous-buffer :wk "prev")
"l" '(bufler :wk "list")
"s" '(consult-buffer :wk "switch...")
"S" '(consult-buffer-other-window :wk "switch... (other window)")
"b" '(bury-buffer :wk "bury")
"d" '(kill-current-buffer :wk "kill")
"w" '(save-buffer :wk "save"))c - Commenting
(autoload 'sp-mark-sexp "smartparens")
(defun comment-sexp ()
"Comment the sexp at point."
(interactive)
(sp-mark-sexp)
(call-interactively #'comment-region))(leader-set-key :infix "c"
"" '(nil :wk "comments")
"l" '(evilnc-comment-or-uncomment-lines :wk "line")
"r" '(comment-or-uncomment-region :wk "region")
"s" '(comment-sexp :wk "sexp"))e - Errors and Flycheck
(autoload 'flycheck-list-errors "flycheck")
(defun flycheck-toggle-error-list ()
"Show or hide the error list."
(interactive)
(if-let* ((window (seq-find (lambda (it)
(equal flycheck-error-list-buffer
(buffer-name (window-buffer it))))
(window-list))))
(delete-window window)
(flycheck-list-errors)))(leader-set-key :infix "e"
"" '(nil :wk "errors")
"n" '(flycheck-next-error :wk "next")
"p" '(flycheck-previous-error :wk "prev")
"l" '(flycheck-toggle-error-list :wk "list")
"r" '(flycheck-buffer :wk "run checks")
"c" '(flycheck-clear :wk "clear")
"e" '(flycheck-explain-error-at-point :wk "explain at pt")
"h" '(flycheck-describe-checker :wk "describe checker")
"s" '(flycheck-select-checker :wk "select checker")
"v" '(flycheck-verify-setup :wk "verify setup"))f - Files
(autoload 'projectile-project-p "projectile")
(autoload 'projectile-invalidate-cache "projectile")
(defun delete-current-buffer-and-file ()
"Remove the file associated with the current buffer, then kill it."
(interactive)
(let ((file (buffer-file-name)))
(cond
((null file)
(kill-buffer))
((not (file-exists-p file))
(kill-buffer))
((yes-or-no-p "Delete this file? ")
(delete-file file t)
(kill-buffer)
(when (projectile-project-p)
(call-interactively #'projectile-invalidate-cache))
(message "File deleted: %s" file)))))
(defun sudo-edit (&optional arg)
"Reopen the current file as sudo for editing.
With prefix argument ARG, prompt for a file."
(interactive "p")
(let* ((fname (if (or arg (not buffer-file-name))
(read-file-name "File: ")
buffer-file-name))
(target (cond ((string-match-p "^/ssh:" fname)
(with-temp-buffer
(insert fname)
(search-backward ":")
(let ((last-match-end nil)
(last-ssh-hostname nil))
(while (string-match "@\\\([^:|]+\\\)" fname last-match-end)
(setq last-ssh-hostname (or (match-string 1 fname)
last-ssh-hostname))
(setq last-match-end (match-end 0)))
(insert (format "|sudo:%s" (or last-ssh-hostname "localhost"))))
(buffer-string)))
(t (concat "/sudo:root@localhost:" fname)))))
(find-file target)))
(defun assert-file-exists-for-buffer (&optional buf)
(let ((cur (buffer-file-name buf)))
(if (not (and cur (file-exists-p cur)))
(error "Buffer is not visiting a file!")
cur)))
(defun rename-file-and-buffer--vc-rename (src dest)
(condition-case err
(when (vc-backend src)
(vc-rename-file src dest)
t)
(error
(let ((msg (error-message-string err)))
(cond
((string-match-p "New file already exists" msg) nil)
((string-match-p "Please update files" msg)
(unless (y-or-n-p "VC cannot track this change automatically. Continue? ")
(error msg)))
(t
(error msg)))))))
(autoload 'recentf-cleanup "recentf")
(require 'subr-x)
(defun rename-file-and-buffer--try (src dest)
(when (and (file-exists-p dest) (not (y-or-n-p "File exists. Overwrite? ")))
(user-error "Aborted"))
(rename-file src dest t)
(when-let* ((buf (get-file-buffer src)))
(with-current-buffer buf
(rename-buffer dest)
(set-visited-file-name dest)
(set-buffer-modified-p nil))
(recentf-cleanup)
(when (projectile-project-p)
(projectile-invalidate-cache nil))))
(autoload 'f-join "f")
;;;###autoload
(defun rename-file-and-buffer (buffer dest-dir dest-filename)
"Rename the current buffer and file it is visiting.
Performs basic VC cleanup.
BUFFER is the buffer to rename.
DEST-DIR is the directory to move the underlying file to.
DEST-FILENAME is the new filename for the underlying file."
(interactive (let ((cur (assert-file-exists-for-buffer)))
(list (current-buffer)
(read-directory-name "Move to directory: " (file-name-directory cur))
(read-string "New name: " (file-name-nondirectory cur)))))
(let ((src (assert-file-exists-for-buffer buffer))
(dest-path (f-join dest-dir dest-filename)))
(or (rename-file-and-buffer--vc-rename src dest-path)
(rename-file-and-buffer--try src dest-path))
(when (and (fboundp 'projectile-project-p) (projectile-project-p))
(call-interactively #'projectile-invalidate-cache))
(message "File '%s' moved to '%s'"
(abbreviate-file-name (file-name-nondirectory src))
(abbreviate-file-name dest-path))))
(defun reload-file ()
"Revisit the current file."
(interactive)
(when-let* ((path (buffer-file-name)))
(find-alternate-file path)))
(defun copy-buffer-path ()
"Show and copy the full path to the current file in the minibuffer."
(interactive)
;; list-buffers-directory is the variable set in dired buffers
(if-let* ((path (or (buffer-file-name) list-buffers-directory)))
(message (kill-new path))
(error "Buffer not visiting a file")))
(defun copy-buffer-name ()
"Show and copy the full path to the current file in the minibuffer."
(interactive)
(let ((name (if-let* ((path (buffer-file-name)))
(file-name-nondirectory path)
(buffer-name))))
(message (kill-new name))))
(defun copy-buffer-directory ()
"Show and copy the directory of the current file in the minibuffer."
(interactive)
;; list-buffers-directory is the variable set in dired buffers
(if-let* ((path (or (ignore-errors (file-name-directory (buffer-file-name))) list-buffers-directory)))
(message (kill-new path))
(error "Buffer not visiting a file")))(leader-set-key :infix "f"
"" '(nil :wk "files")
"d" '(copy-buffer-directory :wk "copy dir")
"y" '(copy-buffer-path :wk "copy path")
"Y" '(copy-buffer-name :wk "copy name")
"D" '(delete-current-buffer-and-file :wk "delete buf & file")
"e" 'sudo-edit
"f" '(find-file :wk "find...")
"F" '(find-file-other-window :wk "find... (other window)")
"s" '(save-buffer :wk "save")
"S" '(save-some-buffers :wk "save... (interactive)")
"l" '(find-file-literally :wk "find literally...")
"l" '(hexl-find-file :wk "find as hex...")
"w" '(write-file :wk "write copy...")
"v" '(reload-file :wk "reload from disk")
"r" '(consult-recent-file :wk "recent files...")
"R" '(rename-file-and-buffer :wk "rename..."))g - Git & Goto
(require 's)
(require 'xref)
(autoload 'projectile-find-file "projectile")
(autoload 'xref-push-marker-stack "xref")
(defun jump-to-file (file &optional pos)
(xref-push-marker-stack)
(let ((buf (or (get-buffer file) (find-file-noselect file))))
(switch-to-buffer buf)
(when pos
(goto-char pos))))
(defun jump-to-config-file ()
"Jump to the config.org file."
(interactive)
(jump-to-file (expand-file-name "config.org" user-emacs-directory)))
(defun jump-to-tangled-config-file ()
"Jump to the config.el file."
(interactive)
(jump-to-file (expand-file-name "config.el" user-emacs-directory)))
(defun jump-to-packages-file ()
"Jump to the packages.nix file."
(interactive)
(jump-to-file (expand-file-name "packages.nix" user-emacs-directory)))
(defun jump-to-init-file ()
"Open the Emacs init.el file."
(interactive)
(jump-to-file (expand-file-name "init.el" user-emacs-directory)))
(defun jump-to-nix-config ()
"Open a nix config file."
(interactive)
(let ((default-directory paths-nix-directory))
(projectile-find-file)))
(defun hostname ()
(cadr (s-match (rx (group (+? nonl)) (? "-" (+ digit)) (? ".local") eos)
(downcase (system-name)))))
(defun jump-to-nix-system-config ()
"Open the nix system config file."
(interactive)
(jump-to-file (format (f-join paths-nix-directory (concat (hostname) ".nix")))))
(defun jump-to-site-file ()
"Open the Emacs site config file."
(interactive)
(jump-to-file user-site-file))
(defun jump-to-messages ()
"Open the messages buffer."
(interactive)
(display-buffer "*Messages*"))(leader-set-key :infix "g"
"" '(nil :wk "git/goto")
"c" '(jump-to-config-file :wk "to config.org")
"C" '(jump-to-tangled-config-file :wk "to tangled config")
"i" '(jump-to-init-file :wk "to init file")
"n" '(jump-to-nix-config :wk "to Nix config")
"S" '(jump-to-nix-system-config :wk "to system Nix config")
"p" '(jump-to-packages-file :wk "to packages.nix")
"?" '(jump-to-messages :wk "to messages buf")
"S" '(jump-to-site-file :wk "to site.el")
"s" '(magit-status :wk "magit")
"d" '(magit-diff-buffer-file :wk "git diff of file")
"b" '(magit-blame :wk "git blame")
"r" '(browse-at-remote :wk "file at git remote")
"l" '(magit-log-buffer-file :wk "git log")
"w" '(magit-worktree-status :wk "git worktree...")
"W" '(magit-worktree :wk "git worktree popup...")
"g" '(xref-find-definitions :wk "find defs")
"G" '(xref-find-definitions-other-window :wk "find def (other window)")
"m" '(xref-find-references :wk "find references")
"SPC" 'pop-tag-mark)h - Help
(leader-set-key :infix "h"
"" '(nil :wk "help")
"i" #'info
"m" #'man
"d" '(nil :wk "describe")
"d c" '(describe-face :wk "face...")
"d C" '(helpful-command :wk "command...")
"d f" '(helpful-callable :wk "function...")
"d k" '(helpful-key :wk "key...")
"d m" '(describe-mode :wk "mode")
"d p" '(describe-text-properties :wk "properties at pt")
"d v" '(helpful-variable :wk "variable...")
"f" '(nil :wk "find")
"f c" '(find-face-definition :wk "face...")
"f f" '(find-function :wk "function...")
"f l" '(find-library :wk "lisp library...")
"f v" '(find-variable :wk "variable..."))k - Killing
(leader-set-key :infix "k"
"" '(nil :wk "kill")
"b" 'kill-this-buffer
"w" 'delete-window
"r" 'consult-yank-pop)l - LSP
(leader-set-key :infix "l"
"" '(:keymap lsp-command-map :package lsp-mode :wk "lsp"))Use which-key replacements
Disabled for now since this breaks insert state in evil :/
(use-package lsp-mode
:custom
(lsp-keymap-prefix "SPC l")
:hook
(lsp-mode . lsp-enable-which-key-integration))n - Narrowing
(leader-set-key :infix "n"
"" '(nil :wk "narrow")
"e" '(edit-indirect-region :wk "edit (indirect)")
"f" '(narrow-to-defun :wk "defun")
"r" '(narrow-to-region :wk "region")
"w" 'widen
"s" '(org-narrow-to-subtree :wk "subtree")
"S" '(org-tree-to-indirect-buffer :wk "tree to indirect buffer"))o - org-mode
(autoload 'org-ref-bibtex-hydra/body "org-ref-bibtex")
(defun jump-to-index-file ()
(interactive)
(org-roam-node-visit (org-roam-node-from-id org-roam-index-node-id)))
(leader-set-key :infix "o"
"" '(nil :wk "org")
"SPC" '(org-roam-dailies-capture-today :wk "capture note...")
"$" '(org-funcs-goto-accounts :wk "accounts")
"/" '(org-ql-search :wk "search...")
"a" '(org-funcs-agenda-dwim :wk "agenda")
"c" '(nil :wk "clock")
"c i" '(timekeep-start :wk "punch in")
"c o" '(timekeep-stop :wk "punch out")
"c r" '(org-resolve-clocks :wk "resolve clocks")
"c g" '(org-clock-goto :wk "goto last clock")
"i" '(jump-to-index-file :wk "index file")
"k" '(org-capture :wk "capture...")
"l" '(org-store-link :wk "store link")
"s" '(org-search-view :wk "search...")
"f" '(org-funcs-roam-node-find :wk "roam file...")
"n" '(org-roam-dailies-goto-today :wk "dailies: today")
"T" '(org-roam-dailies-goto-tomorrow :wk "dailies: tomorrow")
"y" '(org-roam-dailies-goto-yesterday :wk "dailies: yesterday")
"d" '(org-roam-dailies-goto-date :wk "dailies: date...")
"b" '(helm-bibtex :wk "bibliography...")
"r" '(org-ref-bibtex-hydra/body :wk "reference actions...")
"u" '(org-funcs-url-to-reference :wk "create reference of URL...")
"p" '(org-funcs-goto-todos :wk "todos")
"g" '(org-capture-goto-last-stored :wk "last captured")
"t" '(org-funcs-todo-list :wk "todo list")
"v" '(org-tags-view :wk "tags")
"w" '(timekeep-find-client-buffer :wk "work"))p - Projects
(leader-set-key :infix "p"
"" '(nil :wk "projects")
"<tab>" '(projectile-toggle-between-implementation-and-test :wk "toggle impl/test")
"<backtab>" '(projectile-find-implementation-or-test-other-window :wk "find impl/test")
"!" '(projectile-run-async-shell-command-in-root :wk "shell command...")
"c" '(projectile-compile-project :wk "compile...")
"u" '(projectile-run-project :wk "run...")
"t" '(projectile-test-project :wk "test...")
"p" '(projectile-switch-project :wk "switch...")
"f" '(projectile-find-file :wk "find file...")
"d" '(projectile-find-dir :wk "find dir...")
"b" '(projectile-switch-to-buffer :wk "switch buffer...")
"D" '(projectile-dired :wk "dired")
"/" '(consult-ripgrep :wk "search (rg)")
"r" '(projectile-replace :wk "replace"))t - Toggles
(leader-set-key :infix "t"
"" '(nil :wk "toggle")
"i" '(toggle-input-method :wk "input method")
"c" '(hide/show-comments-toggle :wk "comments")
"m" '(global-hide-mode-line-mode :wk "mode line"))w - Windows
(defun split-window-horizontally-dwim (&optional arg)
"When splitting window, show the other buffer in the new window.
With prefix arg ARG, don't select the new window."
(interactive "P")
(split-window-horizontally)
(let ((target-window (next-window)))
(set-window-buffer target-window (other-buffer))
(unless arg
(select-window target-window))))
(defun split-window-vertically-dwim (&optional arg)
"When splitting window, show the other buffer in the new window.
With prefix arg ARG, don't select the new window."
(interactive "P")
(split-window-vertically)
(let ((target-window (next-window)))
(set-window-buffer target-window (other-buffer))
(unless arg
(select-window target-window))))
(defun toggle-window-dedication ()
"Toggle whether the current window is dedicated to its current buffer."
(interactive)
(let* ((window (selected-window))
(was-dedicated (window-dedicated-p window)))
(set-window-dedicated-p window (not was-dedicated))
(message "Window %sdedicated to %s"
(if was-dedicated "no longer " "")
(buffer-name))))(leader-set-key :infix "w"
"" '(nil :wk "window")
"w" '(evil-window-next :wk "next")
"r" '(evil-window-rotate-downwards :wk "rotate")
"/" '(split-window-horizontally-dwim :wk "split (horizontal)")
"-" '(split-window-vertically-dwim :wk "split (vertical)")
"=" '(balance-windows :wk "balance")
"d" '(delete-window :wk "delete")
"o" '(delete-other-windows :wk "delete others")
"t" '(toggle-window-dedication :wk "toggle dedication"))y - Text snippets
(leader-set-key :infix "y"
"" '(nil :wk "snippets")
"n" '(yas-new-snippet :wk "new")
"e" '(yas-expand :wk "expand")
"f" '(yas-visit-snippet-file :wk "open...")
"y" '(yas-insert-snippet :wk "insert..."))z - Text Scale
(leader-set-key :infix "z"
"" '(nil :wk "zoom")
"+" '(default-text-scale-increase :wk "increase text scale")
"-" '(default-text-scale-decrease :wk "decrease text scale")
"=" '(default-text-scale-reset :wk "reset text scale"))Unbind SPC in magit-section for compatability
(use-package magit-section
:general (:keymaps 'magit-section-mode-map "SPC" nil))Use ~,~ for mode-specific commands
(use-package general
:after evil
:demand t
:config
(general-define-key :states '(normal motion) "," nil))
(defmacro mode-leader-set-key (&rest args)
(declare (indent defun))
`(use-package general
:after evil
:demand t
:config
(,'general-def ,@args ,@'(:keymaps 'override :states
'(normal motion visual)
:prefix ","))))Search & replace
deadgrep - Ripgrep (rg) frontend
(use-package deadgrep
:general (:keymaps 'deadgrep-mode-map "C-c C-w" #'deadgrep-edit-mode)
:init
(defalias 'rg #'deadgrep)
:config
(setq-default deadgrep--search-type 'regexp))Use c in the deadgrep buffer to change the search term
(use-package deadgrep
:preface
(defun config-deadgrep--requery ()
(interactive)
(let ((button (save-excursion
(goto-char (point-min))
(forward-button 1))))
(button-activate button)))
:general (:states 'normal :keymaps 'deadgrep-mode-map
"c" #'config-deadgrep--requery))Provide feedback in the echo area on entering and exiting deadgrep-edit-mode
(use-package deadgrep
:config
(defun config-deadgrep--on-exit-edit-mode (&rest _)
(when (derived-mode-p 'deadgrep-edit-mode)
(let ((message-log-max))
(message "Exiting edit mode."))))
(defun config-deadgrep--on-enter-edit-mode (&rest _)
(let ((message-log-max))
(message "Entering edit mode. Changes will be made to underlying files as you edit.")))
(advice-add 'deadgrep-mode :before #'config-deadgrep--on-exit-edit-mode)
(advice-add 'deadgrep-edit-mode :after #'config-deadgrep--on-enter-edit-mode))wgrep - Directly edit grep results
Enable wgrep, which provides editable grep buffers.
(use-package wgrep)Prompts and UI enhancements
historian - Persistent input history
(use-package historian
:hook (after-init . historian-mode))orderless - order-insensitive matching algorithm
https://github.com/oantolin/orderless
(use-package orderless
:custom
(completion-styles '(orderless)))selectrum - Incremental search and narrowing
(use-package selectrum
:hook (after-init . selectrum-mode))Integrate selectrum with orderless
(use-package orderless
:custom
(orderless-skip-highlighting (lambda () (bound-and-true-p selectrum-is-active)))
(selectrum-highlight-candidates-function #'orderless-highlight-matches))KLUDGE: Use C-h in find-file to traverse up a dir
It would be nice if this was an out-of-the-box thing.
See: Add dedicated command to move up a directory in find-file · Issue #498 · raxo…
(defun selectrum-up (&optional arg)
(interactive "p")
(save-restriction
(narrow-to-region (minibuffer-prompt-end) (point-max))
(let ((start (point)))
(forward-sexp (- (or arg 1)))
(delete-region start (point)))))(use-package selectrum
:general
(:keymaps 'selectrum-minibuffer-map "C-h" 'selectrum-up))Teach find-file to understand ~ and //
Teach find-file how to navigate up past the home dir
consult - Commands built on top of selectrum
(use-package consult
:custom
(consult-project-root-function 'projectile-project-root)
:general (:states 'normal "/" 'consult-line))marginalia - adds helpful annotations to completion prompts
(use-package marginalia
:hook (after-init . marginalia-mode))Window management
winner - Window state history
winner-mode saves the window and buffer layout history, allowing you to cycle
forward and back through layout states. This is useful for recovering a layout
after editing actions have changed what windows are shown.
(use-package winner
:general ("<C-left>" 'winner-undo
"<C-right>"'winner-redo)
:hook (after-init . winner-mode)
:custom
(winner-boring-buffers '("*Completions*"
"*Compile-Log*"
"*inferior-lisp*"
"*Fuzzy Completions*"
"*Apropos*"
"*Help*"
"*cvs*"
"*Buffer List*"
"*Bufler*"
"*esh command on file*")))rotate - Rotate buffers within window layout
rotate provides handy commands for manipulating the window layout.
(use-package rotate
:commands (rotate-layout))popper - Categorise buffers for window assignments
(use-package popper
:hook (after-init . popper-mode)
:custom
(popper-mode-line nil)
(popper-group-function 'popper-group-by-projectile)
(popper-reference-buffers (list (rx bol "*Messages*")
(rx bol "*ielm*")
(rx bol "*" (? "Async ") "Shell Command")
'help-mode
'occur-mode
'compilation-mode)))Text completion engines
hippie-expand - generic text completion
hippie-expand is a generic completion engine that works in most buffers without
any special language-level support.
Use hippie-expand as the default completion command for evil
(use-package hippie-exp
:general ("M-/" 'hippie-expand
:states 'insert
[remap evil-complete-previous] 'hippie-expand))Set the default heuristic for completing symbols
(use-package hippie-exp
:custom
(hippie-expand-try-functions-list
'(try-expand-dabbrev
try-expand-dabbrev-all-buffers
try-expand-dabbrev-from-kill
try-complete-file-name-partially
try-complete-file-name
try-expand-all-abbrevs
try-expand-list
try-expand-line
try-complete-lisp-symbol-partially
try-complete-lisp-symbol)))company - UI for selecting completions
company is a general-purpose completion frontend, showing a popup of completion
options.
(use-package company
:hook (after-init . global-company-mode)
:general
([remap completion-at-point] #'company-manual-begin
[remap complete-symbol] #'company-manual-begin)
(:states '(insert normal emacs) :keymaps 'company-active-map
"S-<return>" #'company-complete
"<return>" #'company-complete-selection)
(:keymaps 'comint-mode-map [remap indent-for-tab-command] #'company-manual-begin)
:preface
(general-unbind :keymaps 'company-active-map "C-w" "C-h")
:custom
(company-idle-delay 0.3)
(company-minimum-prefix-length 3)
(company-tooltip-align-annotations t)
(company-require-match nil)
:config
(require 'company-tng))evil-collection-company seems to be messing with the <return> binding, so I need
to manually apply it again.
(use-package company
:after evil-collection
:config
(defun config-company--set-company-vars ()
(define-key company-active-map (kbd "RET") #'company-complete-selection))
(add-hook 'company-mode-hook #'config-company--set-company-vars))Themeing
volatile-highlights - Highlight pasted text
Load at compile-time so macro expansions are available
(cl-eval-when (compile)
(require 'volatile-highlights))Configure the package
(use-package volatile-highlights
:hook
(prog-mode . (lambda () (require 'volatile-highlights)))
(text-mode . (lambda () (require 'volatile-highlights)))
:config
(volatile-highlights-mode))Highlight text pasted by evil operations
(use-package volatile-highlights
:after (evil)
:demand t
:config
(vhl/define-extension 'evil
'evil-move
'evil-paste-after
'evil-paste-before
'evil-paste-pop)
(vhl/install-extension 'evil)
(vhl/load-extension 'evil))highlight-thing - Highlight the symbol at point
(use-package highlight-thing
:hook (prog-mode . highlight-thing-mode)
:custom
(highlight-thing-what-thing 'symbol)
(highlight-thing-delay-seconds 0.1)
(highlight-thing-limit-to-defun nil)
(highlight-thing-case-sensitive-p t)
:config
(set-face-attribute 'highlight-thing nil :inherit 'highlight))Suppress highlight-thing when hovering over certain kinds of symbols
(use-package highlight-thing
:config
(defun config-highlight-thing--should-highlight-p (res)
(unless (bound-and-true-p lsp-ui-mode)
(when res
(let ((excluded-faces '(font-lock-string-face
font-lock-keyword-face
font-lock-comment-face
font-lock-preprocessor-face
font-lock-builtin-face))
(faces (seq-mapcat #'face-ancestors (face-at-point nil t))))
(null (seq-intersection faces excluded-faces))))))
(advice-add 'highlight-thing-should-highlight-p :filter-return
#'config-highlight-thing--should-highlight-p))page-break-lines - Show page breaks characters as a horizontal rule
(use-package page-break-lines
:hook (after-init . global-page-break-lines-mode)
:custom
(page-break-lines-modes '(prog-mode org-agenda-mode latex-mode help-mode)))paren-face - Apply a specific face to parens
(use-package paren-face
:hook (after-init . global-paren-face-mode)
:custom
(paren-face-regexp (rx (any "{}();,")))
:config
(set-face-attribute 'parenthesis nil
:inherit 'font-lock-comment-face
:weight 'light
:italic nil
:background nil)
(add-to-list 'paren-face-modes 'js-mode)
(add-to-list 'paren-face-modes 'lisp-data-mode)
(add-to-list 'paren-face-modes 'typescript-mode)
(add-to-list 'paren-face-modes 'yaml-mode)
(font-lock-add-keywords 'js-mode `((,(rx (any ":")) 0 'parenthesis)))
(font-lock-add-keywords 'typescript-mode `((,(rx (any ":")) 0 'parenthesis))))hl-todo - Highlight TODOs in comments
(use-package hl-todo
:hook ((prog-mode . hl-todo-mode)
(text-mode . enable-hl-todo-unless-org-buffer))
:preface
(defun enable-hl-todo-unless-org-buffer ()
(unless (derived-mode-p 'org-mode)
(hl-todo-mode)))
:custom
(hl-todo-keyword-faces
(seq-map (lambda (it) (cons it 'hl-todo))
'("TODO"
"NEXT"
"HACK"
"FIXME"
"KLUDGE"
"PATCH"
"NOTE"))))which-key - Show keys after a delay on input
(use-package which-key
:hook (after-init . which-key-mode)
:custom
(which-key-sort-uppercase-first nil)
(which-key-idle-delay 0.4))default-text-scale - Commands for changing text scale for all buffers simultaneously
(use-package default-text-scale
:custom
(default-text-scale-amount 30))minions - Hides most minor modes behind a menu
(use-package minions
:demand t
:custom
(minions-mode-line-lighter "...")
(minions-direct '(auto-revert-mode git-auto-commit-mode flycheck-mode))
:config
(minions-mode +1))File & Buffer management
bufler - Better buffer list
(use-package bufler
:commands (bufler bufler-list)
:config
(evil-set-initial-state 'bufler-list-mode 'motion)
(dolist (mode '(org-agenda-mode flycheck-error-message-mode magit-status-mode))
(add-to-list 'bufler-filter-buffer-modes mode))
:general
("C-x C-b" 'bufler-list)
(:states '(motion) :keymaps 'bufler-list-mode-map
"1" 'magit-section-show-level-1
"2" 'magit-section-show-level-2
"3" 'magit-section-show-level-3
"4" 'magit-section-show-level-4
"<" 'beginning-of-buffer
"<backtab>" 'magit-section-cycle-global
"q" 'quit-window
">" 'end-of-buffer
"?" 'hydra:bufler/body
"C-<tab>" 'magit-section-cycle
"DEL" 'scroll-down-command
"F" 'bufler-list-group-make-frame
"M-1" 'magit-section-show-level-1-all
"M-2" 'magit-section-show-level-2-all
"M-3" 'magit-section-show-level-3-all
"M-4" 'magit-section-show-level-4-all
"M-<tab>" 'magit-section-cycle
"M-n" 'magit-section-forward-sibling
"M-p" 'magit-section-backward-sibling
"N" 'bufler-list-buffer-name-workspace
"RET" 'bufler-list-buffer-switch
"TAB" 'magit-section-toggle
"^" 'magit-section-up
"f" 'bufler-list-group-frame
"g r" 'bufler
"d" 'bufler-list-buffer-kill
"n" 'magit-section-forward
"p" 'magit-section-backward
"q" 'quit-window
"s" 'bufler-list-buffer-save))dired - Filesystem browsing
dired is the builtin filesystem browser for Emacs.
(use-package dired
:hook (dired-mode . dired-hide-details-mode)
:general
(:states 'normal :keymaps 'dired-mode-map
"$" #'end-of-line
"i" nil
"i i" 'dired-insert-subdir
"i q" 'dired-kill-subdir
"TAB" 'dired-hide-subdir)
:custom
(dired-listing-switches "-alhv")
(dired-dwim-target t)
(dired-auto-revert-buffer t)
(dired-hide-details-hide-symlink-targets nil)
(dired-omit-files (rx bol "."))
:config
(add-hook 'dired-mode-hook #'hl-line-mode)
(put 'dired-find-alternate-file 'disabled nil))Set leader keys
(mode-leader-set-key :keymaps 'dired-mode-map
"?" '(dired-hide-details-mode :wk "toggle details")
"." '(dired-omit-mode :wk "toggle hidden")
"e" '(wdired-change-to-wdired-mode :wk "wdired")
"s" '(dired-sort-toggle-or-edit :wk "toggle sort")
"f" 'dired
"F" '(dired-other-window :wk "dired (other window)")
"m" '(nil :wk "mark")
"m a" '(dired-mark-unmarked-files :wk "unmarked")
"m c" '(dired-change-marks :wk "change")
"m r" '(dired-mark-files-regexp :wk "by regexp")
"m l" '(dired-mark-symlinks :wk "symlinks")
"m d" '(dired-mark-directories :wk "directories")
"U" '(dired-unmark-all-marks :wk "unmark all")
"!" '(dired-do-shell-command :wk "shell command...")
"d" '(nil :wk "execute (marked)")
"d c" '(dired-do-copy :wk "copy")
"d D" '(dired-do-delete :wk "delete")
"d h" '(dired-do-hardlink :wk "hardlink")
"d s" '(dired-do-relsymlink :wk "symlink (relative)")
"d S" '(dired-do-symlink :wk "symlink (absolute)")
"d /" '(dired-do-search :wk "search"))Put directories first in sort order
(use-package dired
:config
(defun config-dired--sort-directories-first (&rest _)
"Sort dired listings with directories first."
(save-excursion
(let (buffer-read-only)
(forward-line 2) ;; beyond dir. header
(sort-regexp-fields t "^.*$" "[ ]*." (point) (point-max)))
(set-buffer-modified-p nil)))
(advice-add 'dired-readin :after #'config-dired--sort-directories-first))Rename files by editing dired buffer
wdired is a mode that allows you to rename files and directories by editing the
dired buffer itself.
(use-package wdired
:general
(:states 'normal
:keymaps 'wdired-mode-map "^" #'evil-first-non-blank
:keymaps 'dired-mode-map "C-c C-e" #'wdired-change-to-wdired-mode))Toggle visibility of hidden files
Use dired-x to toggle visibility of ‘hidden’ files (i.e. files starting with a
dot).
(use-package dired-x
:demand t
:after dired
:hook (dired-mode . dired-omit-mode)
:general
(:states 'normal :keymaps 'dired-mode-map "h" #'dired-omit-mode)
:custom
(dired-omit-verbose nil)
(dired-clean-up-buffers-too t))projectile - Project and repo-level commands
projectile provides commands for working with projects, and a useful utility
function to find the root directory of the project.
Emacs now comes with project.el, but it provides a subset of the functionality
of projectile. Use projectile until the builtin functionality is more complete.
(use-package projectile
:hook (after-init . projectile-mode)
:custom
(projectile-project-search-path paths-project-directories)
(projectile-switch-project-action (lambda () (dired default-directory)))
(projectile-enable-caching t)
(projectile-create-missing-test-files t)
(projectile-globally-ignored-files '("TAGS" ".DS_Store"))
(projectile-globally-ignored-file-suffixes
'("meta"
"gz"
"zip"
"tar"
"tgz"
"elc"
"eln"))
(projectile-globally-ignored-directories
'("coverage"
".bzr"
".eunit"
".fslckout"
".g8"
".git"
".hg"
".svn"
"dist"
"jars"
"node_modules"
"vendor"
"target")))Git
magit - interactive commands for working with git
(use-package magit
:general
(:keymaps 'transient-base-map "<escape>" #'transient-quit-one
:states 'normal :keymaps 'magit-refs-mode-map "." #'magit-branch-and-checkout)
:custom
(magit-repository-directories (--map (cons it 1) paths-project-directories))
(magit-display-buffer-function 'magit-display-buffer-same-window-except-diff-v1)
(magit-log-section-commit-count 0))Reveal the entire org buffer when blaming or visiting from a diff
(use-package magit
:after (org)
:config
(defun config-git--reveal-org-buffer ()
(when (derived-mode-p 'org-mode)
(org-reveal t)))
(add-hook 'magit-diff-visit-file-hook 'config-git--reveal-org-buffer)
(add-hook 'magit-blame-mode-hook #'config-git--reveal-org-buffer))GPG verification
Hack magit’s commit info to show output of a GPG signature check.
(use-package magit-gpg
:after (magit)
:demand t
:commands (magit-gpg-insert-revision-gpg)
:preface
(autoload 'magit-add-section-hook "magit")
(autoload 'magit-insert-revision-headers "magit")
:config
(magit-add-section-hook 'magit-revision-sections-hook
#'magit-gpg-insert-revision-gpg
#'magit-insert-revision-headers
t))Improve magit performance on macOS
See: https://github.com/magit/magit/commit/26e064e1d78acb4d4d422a0a5743609612863caa
(when (equal system-type 'darwin)
(setq magit-git-executable (expand-file-name "~/.nix-profile/bin/git")))Compatibility with selectrum
(use-package magit
:after (selectrum)
(setq magit-completing-read-function #'selectrum-completing-read))forge - teaches magit how to work with pull requests and issues
- Note taken on [2021-08-07 Sat 15:27]
Temporarily disabled while yaml dependency has build issues on macOS
(use-package forge
:after magit
:demand t
:config
(remove-hook 'magit-status-sections-hook 'forge-insert-issues)
(add-hook 'magit-status-sections-hook 'forge-insert-requested-reviews 90)
(add-hook 'magit-status-sections-hook 'forge-insert-assigned-issues 90))git-auto-commit-mode - Commit files on save
(use-package git-auto-commit-mode
:delight " auto-commit"
:hook (pass-mode . git-auto-commit-mode)
:custom
(gac-debounce-interval 10)
(gac-silent-message-p t)
(gac-automatically-push-p t)
(gac-automatically-add-new-files-p t))vc-annotate - Step through file history
(use-package vc-annotate
:general
(:states 'normal :keymaps 'vc-annotate-mode-map
"<return>" 'vc-annotate-find-revision-at-line
"<tab>" 'vc-annotate-goto-line
"n" 'vc-annotate-next-revision
"f" 'vc-annotate-next-revision
"l" 'vc-annotate-show-log-revision-at-line
"p" 'vc-annotate-prev-revision
"b" 'vc-annotate-prev-revision
"d" 'vc-annotate-show-diff-revision-at-line
"D" 'vc-annotate-show-changeset-diff-revision-at-line
"." 'vc-annotate-working-revision))git-commit-mode - Commit messages authoring mode
(use-package git-commit-mode
:init
(defun configure-git-commit-mode ()
(setq-local fill-column 72))
(add-hook 'git-commit-mode-hook 'configure-git-commit-mode))git-commit-ticket-prefix - Add ticket number to commit messaging
(use-package git-commit-ticket-prefix
:commands (git-commit-ticket-prefix-insert)
:hook (git-commit-setup . git-commit-ticket-prefix-insert))browse-at-remote - Browse file at remote
browse-at-remote provides commands for opening the current buffer in the source
repo, or copying the remote URL to the clipboard.
(use-package browse-at-remote
:general
("C-x v o" 'browse-at-remote
"C-x v y" 'browse-at-remote-kill)
:custom
(browse-at-remote-add-line-number-if-no-region-selected nil))Provide better feedback by writing to *Messages*
(use-package browse-at-remote
:config
(defun config-browse-at-remote--message-kill (&rest _)
(let ((message-log-max))
(message "Copied to kill ring: %s" (substring-no-properties (car kill-ring)))))
(advice-add 'browse-at-remote-kill :after 'config-browse-at-remote--message-kill))Programming languages & text-modes
flycheck - Syntax Checking & Linting
Flycheck integrates with external tools to show indications of errors and
warnings in the buffer as you edit.
See: flycheck.org
(use-package flycheck
:hook
(after-init . global-flycheck-mode)
(prog-mode . flycheck-mode-on-safe)
:general
(:keymaps
'flycheck-mode-map
"M-n" #'flycheck-next-error
"M-p" #'flycheck-previous-error
"M-j" #'flycheck-next-error
"M-k" #'flycheck-previous-error)
(:states 'motion
:keymaps 'flycheck-error-list-mode-map
"j" #'flycheck-error-list-next-error
"k" #'flycheck-error-list-previous-error
"RET" #'flycheck-error-list-goto-error
"n" #'flycheck-error-list-next-error
"p" #'flycheck-error-list-previous-error
"q" #'quit-window)
:custom
(flycheck-display-errors-delay 0.1)
(flycheck-emacs-lisp-load-path 'inherit)
(flycheck-python-pycompile-executable "python")
(flycheck-global-modes '(not text-mode
org-mode
org-agenda-mode)))Show the Flycheck error list in a bottom window.
(add-to-list 'display-buffer-alist
`(,(rx bos "*Flycheck errors*" eos)
(display-buffer-reuse-window display-buffer-in-side-window)
(slot . ,display-buffer-slot-diagnostics)
(reusable-frames . visible)
(side . bottom)
(window-height . 0.4)))Customise the modeline indicator
(use-package flycheck
:config
(defun flycheck-custom-mode-line-status-text (&optional status)
(pcase (or status flycheck-last-status-change)
(`no-checker " Checks[-]")
(`errored " Checks[ERROR]")
(`finished
(let-alist (flycheck-count-errors flycheck-current-errors)
(cond
((and .error .warning)
(format " ✖ (%s error%s, %s warn%s)"
.error
(if (equal .error 1) "" "s")
.warning
(if (equal .warning 1) "" "s")))
(.error
(format " ✖ (%s error%s)" .error (if (equal .error 1) "" "s")))
(.warning
(format " ! (%s warning%s)" .warning (if (equal .warning 1) "" "s")))
(t
" ✔"))))
(`interrupted " ? (interrupted)")
(`suspicious " ? (suspicious)")
(_
"")))
:custom
(flycheck-mode-line '(:eval (flycheck-custom-mode-line-status-text))))Projectile integration
Automatically re-check all buffers belonging to a project on save. This ensures diagnostics do not go stale.
(use-package flycheck
:after (projectile)
:config
(defun config-flycheck--check-all-project-buffers ()
(when (and (bound-and-true-p projectile-mode) (projectile-project-p))
(projectile-process-current-project-buffers
(lambda (buf)
(with-current-buffer buf
(when (bound-and-true-p flycheck-mode)
;; HACK: Inhibit checks for elisp, otherwise flycheck will
;; spawn a bunch of thrashing Emacs processes.
(unless (derived-mode-p 'emacs-lisp-mode)
(flycheck-buffer))))))))
(add-hook 'after-save-hook #'config-flycheck--check-all-project-buffers))Conditionally inhibit Flycheck
Don’t use Flycheck in certain situations, such as for files inside node_modules,
during ediff merges, etc.
(use-package flycheck
:config
(defun config-flycheck--maybe-inhibit (result)
(unless (or (equal (buffer-name) "*ediff-merge*")
(string-suffix-p ".dir-locals.el" (buffer-file-name))
(string-match-p (rx bol "*Pp ") (buffer-name))
(string-match-p (rx "/node_modules/") default-directory))
result))
(advice-add 'flycheck-may-enable-mode :filter-return #'config-flycheck--maybe-inhibit))smartparens - Structured expression editing
Use smartparens to keep parens and braces paired and manipulate expressions in a
structured way.
See: Fuco1/smartparens
Set general variables
(use-package smartparens
:hook
(prog-mode . smartparens-strict-mode)
(text-mode . smartparens-strict-mode)
:general
(:keymaps 'smartparens-strict-mode-map
[remap c-electric-backspace] #'sp-backward-delete-char)
(:states 'insert
")" #'sp-up-sexp)
(:states 'normal
"D" #'sp-kill-hybrid-sexp)
:custom
(sp-show-pair-delay 0.2)
(sp-show-pair-from-inside t)
(sp-cancel-autoskip-on-backward-movement nil)
(sp-highlight-pair-overlay nil)
(sp-highlight-wrap-overlay nil)
(sp-highlight-wrap-tag-overlay nil)
(sp-navigate-close-if-unbalanced t)
(sp-message-width nil)
:config
(require 'smartparens-config)
(smartparens-global-strict-mode +1)
(show-smartparens-global-mode +1))Load macros and functions at compile time so I can use them in this config
(cl-eval-when (compile)
(require 'smartparens))
(autoload 'sp-pair "smartparens")
(autoload 'sp-local-pair "smartparens")Define utility functions
(autoload 'sp-get-pair "smartparens")
(autoload 'sp--get-opening-regexp "smartparens")
(autoload 'sp--get-closing-regexp "smartparens")
(defun config-smartparens-add-space-before-sexp-insertion (id action _context)
(when (eq action 'insert)
(save-excursion
(backward-char (length id))
(cond
((and (eq (preceding-char) ?$)
(equal id "{")))
((eq (char-syntax (preceding-char)) ?w)
(just-one-space))
((and (looking-back (sp--get-closing-regexp) (line-beginning-position))
(not (eq (char-syntax (preceding-char)) ?')))
(just-one-space))))))
(defun config-smartparens-add-space-after-sexp-insertion (id action _context)
(when (eq action 'insert)
(save-excursion
(forward-char (sp-get-pair id :cl-l))
(when (or (eq (char-syntax (following-char)) ?w)
(looking-at (sp--get-opening-regexp)))
(insert " ")))))Define the pairs to use by default in all modes
(use-package smartparens
:config
(sp-pair "`" "`"
:bind "M-`")
(sp-pair "{" "}"
:bind "M-{"
:pre-handlers '(config-smartparens-add-space-before-sexp-insertion)
:post-handlers '(("||\n[i]" "RET") ("| " "SPC")))
(sp-pair "[" "]"
:bind "M-["
:post-handlers '(("||\n[i]" "RET") ("| " "SPC")))
(sp-pair "(" ")"
:bind "M-("
:post-handlers '(("||\n[i]" "RET") ("| " "SPC")))
(sp-pair "\"" "\""
:bind "M-\""
:pre-handlers '(:add (config-smartparens-add-space-before-sexp-insertion))))Delete enclosing whitespace as necessary on backspace
(use-package smartparens
:functions (sp-get-enclosing-sexp)
:config
(defun config-smartparens-delete-horizontal-space-for-delete (f &rest args)
"Perform context-sensitive whitespace cleanups when deleting.
For performance, only consider a subset of the buffer."
(save-restriction
(unless (derived-mode-p 'emacs-lisp-mode)
(apply #'narrow-to-region (bounds-of-surrounding-lines 500 500)))
(-let* ((line-before-pt (buffer-substring (line-beginning-position) (point)))
(line-after-pt (buffer-substring (point) (line-end-position)))
((&plist :beg beg :end end :op op :cl cl) (sp-get-enclosing-sexp))
(inside-start (when op (+ beg (length op))))
(inside-end (when op (- end (length cl))))
(inside (when op
(concat (buffer-substring inside-start (point))
(buffer-substring (point) inside-end)))))
(cond
;; Collapse horizontal space in empty pairs.
;;
;; [ | ] -> [|]
;;
((when op (string-match-p (rx bos (+ space) eos) inside))
(delete-region inside-start inside-end))
;; Delete contents for multiline pairs that were just inserted, e.g. braces.
;;
;; {
;; |
;; }
;;
;; ->
;;
;; {|}
((when op (string-match-p (rx bos (* space) "\n" (* space) "\n" (* space) eos) inside))
(delete-region inside-start inside-end))
;; Delete back from end of the line.
;;
;;
;; foo |
;; ->
;; foo|
;; foo |
;; ->
;; foo |
((string-empty-p line-after-pt)
(if (string-match-p (rx space space eos) line-before-pt)
(while (looking-back (rx space space) (line-beginning-position))
(delete-char -1))
(funcall f args)))
;; Don't aggressively delete whitespace if there's a comment
;; following pt.
;;
;;
;; foo | // bar
;;
;; ->
;;
;; foo| // bar
;;
((string-match-p (rx (* nonl) (syntax comment-start)) line-after-pt)
(funcall f args))
;; Collapse surrounding space, but preserve padding inside pairs.
;;
;; foo | bar -> foo|bar
;;
;; foo | } -> foo| }
;;
((and (string-match-p (rx (or bol (not space)) space eos) line-before-pt)
(string-match-p (rx bos space (or eol (not space))) line-after-pt))
(let ((backward-only? (when inside (string-match-p (rx bos space) inside))))
(delete-horizontal-space backward-only?)))
;; Delete if there is a single preceding space.
;;
;; foo |bar -> foo|bar
;;
;; but not:
;;
;; foo| bar -> foo|bar
;;
((and (string-match-p (rx (or bol (not space)) space eos) line-before-pt)
(string-match-p (rx bos (not space)) line-after-pt))
(delete-char -1))
;; Delete surrounding whitespace beyond a certain length.
;;
;; foo |bar -> foo |bar
;; foo | bar -> foo | bar
((string-match-p (rx (+ space) eos) line-before-pt)
(let ((has-space? (eq (char-after) ? )))
(skip-chars-forward " ")
(while (looking-back (rx space space) (line-beginning-position))
(delete-char -1))
(when has-space?
(insert " ")
(forward-char -1))))
(t
(funcall f args))))))
(advice-add 'sp-backward-delete-char :around #'config-smartparens-delete-horizontal-space-for-delete))Emacs lisp
(use-package smartparens
:config
(sp-with-modes (cons 'lisp-data-mode sp-lisp-modes)
(sp-local-pair "(" nil
:pre-handlers '(config-smartparens-add-space-before-sexp-insertion)
:post-handlers '(config-smartparens-add-space-after-sexp-insertion))
(sp-local-pair "[" nil
:pre-handlers '(config-smartparens-add-space-before-sexp-insertion)
:post-handlers '(config-smartparens-add-space-after-sexp-insertion))
(sp-local-pair "\"" nil
:pre-handlers '(config-smartparens-add-space-before-sexp-insertion)
:post-handlers '(config-smartparens-add-space-after-sexp-insertion))
(sp-local-pair "{" nil
:pre-handlers '(config-smartparens-add-space-before-sexp-insertion)
:post-handlers '(config-smartparens-add-space-after-sexp-insertion))))Don’t pad curly-braces in markdown, org, and latex
(use-package smartparens
:config
(sp-with-modes '(org-mode markdown-mode gfm-mode latex-mode)
(sp-local-pair "{" "}" :pre-handlers nil)))Make checkbox insertion smarter for org-mode and markdown
(use-package smartparens
:config
(autoload 'org-at-item-p "org-list")
(defun config-smartparens--format-checkitem (_id action _context)
(when (and (equal action 'insert)
(org-at-item-p))
(atomic-change-group
(just-one-space)
(search-backward "[" (line-beginning-position))
(just-one-space)
(search-forward "]" (line-end-position))
(just-one-space))))
(sp-with-modes '(org-mode markdown-mode gfm-mode)
(sp-local-pair "[" "]" :post-handlers '(config-smartparens--format-checkitem))))elisp-mode - Emacs Lisp
(use-package elisp-mode
:general
(:keymaps '(emacs-lisp-mode-map lisp-interaction-mode-map)
"C-c C-c" #'eval-defun
"C-c C-b" #'eval-buffer)
(:states 'visual
:keymaps '(emacs-lisp-mode-map lisp-interaction-mode-map)
"C-c C-c" #'eval-region))Set leader keys
(mode-leader-set-key :keymaps 'emacs-lisp-mode-map
"e" '(nil :wk "eval")
"eb" '(eval-buffer :wk "eval buf")
"ee" '(eval-expression :wk "eval expr")
"d" '(nil :wk "debug")
"df" '(debug-on-entry :wk "function...")
"dc" '(nil :wk "cancel... (function)")
"dv" '(debug-on-variable-change :wk "variable...")
"dV" '(cancel-debug-on-variable-change :wk "cancel... (variable)")
"t" 'ert)debug - Emacs Lisp debugger
Set leader keys
(mode-leader-set-key :keymaps 'debugger-mode-map
"," '(debugger-step-through :wk "step")
"b" '(debugger-frame :wk "frame")
"c" '(debugger-continue :wk "continue")
"j" '(debugger-jump :wk "jump")
"u" '(debugger-frame-clear :wk "clear frame")
"e" '(debugger-eval-expression :wk "eval...")
"R" '(debugger-record-expression :wk "record...")
"r" '(debugger-return-value :wk "return...")
"l" '(debugger-list-functions :wk "list functions")
"v" '(debugger-toggle-locals :wk "toggle locals"))Customise how debugger buffer is displayed
(add-to-list 'display-buffer-alist
`((,(rx bos "*Backtrace*" eos)
(display-buffer-reuse-window display-buffer-in-side-window)
(slot . ,display-buffer-slot-diagnostics)
(side . bottom)
(reusable-frames . visible)
(window-height . 0.4))))elisp-slime-nav - Code navigation
Use elisp-slime-nav to go to definition in Emacs Lisp.
(use-package elisp-slime-nav
:hook (emacs-lisp-mode . elisp-slime-nav-mode)
:general
(:keymaps 'emacs-lisp-mode-map :states 'normal
"M-." #'elisp-slime-nav-find-elisp-thing-at-point))helpful - Better help buffer
(use-package helpful
:general
(:keymaps '(emacs-lisp-mode-map helpful-mode-map) :states '(motion normal)
"K" 'helpful-at-point))Display helpful buffers in side windows.
(add-to-list 'display-buffer-alist
`(,(rx bos "*helpful ")
(display-buffer-reuse-window display-buffer-pop-up-window)
(slot . ,display-buffer-slot-documents)
(reusable-frames . visible)
(side . right)
(window-width . 0.5)))ielm - Emacs Lisp REPL
(use-package ielm
:general
(:keymaps 'emacs-lisp-mode-map "C-c C-z" #'ielm)
(:keymaps 'inferior-emacs-lisp-mode-map
"C-c C-z" #'config-elisp-pop-to-elisp-buffer)
:config
(defun config-elisp-pop-to-elisp-buffer ()
(interactive)
(if-let* ((buf (seq-find (lambda (buf)
(with-current-buffer buf
(derived-mode-p 'emacs-lisp-mode)))
(buffer-list))))
(pop-to-buffer buf)
(user-error "No Emacs Lisp buffers")))
(add-hook 'inferior-emacs-lisp-mode-hook #'hs-minor-mode))Display ielm in a side window.
(add-to-list 'display-buffer-alist
`(,(rx bos "*ielm*" eos)
(display-buffer-reuse-window display-buffer-in-side-window)
(slot . ,display-buffer-slot-repls)
(side . right)
(window-width . 80)))pp - S-Expression Pretty-printing
(use-package pp
:general
(:keymaps '(emacs-lisp-mode-map lisp-interaction-mode-map inferior-emacs-lisp-mode-map)
:states '(motion normal insert)
"C-c C-<return>" 'pp-eval-last-sexp
"C-c <return>" 'pp-eval-last-sexp
"C-c e" 'pp-macroexpand-last-sexp
"C-c C-e" 'pp-macroexpand-last-sexp))Improve indent function
Teach the Emacs Lisp indentation function to indent plists nicely.
(use-package lisp-mode
:preface
(progn
(defvar calculate-lisp-indent-last-sexp)
(defun config-elisp--better-lisp-indent-function (indent-point state)
(let ((normal-indent (current-column))
(orig-point (point)))
(goto-char (1+ (elt state 1)))
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
(cond
;; car of form doesn't seem to be a symbol, or is a keyword
((and (elt state 2)
(or (not (looking-at "\\sw\\|\\s_"))
(looking-at ":")))
(unless (> (save-excursion (forward-line 1) (point))
calculate-lisp-indent-last-sexp)
(goto-char calculate-lisp-indent-last-sexp)
(beginning-of-line)
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t))
;; Indent under the list or under the first sexp on the same
;; line as calculate-lisp-indent-last-sexp. Note that first
;; thing on that line has to be complete sexp since we are
;; inside the innermost containing sexp.
(backward-prefix-chars)
(current-column))
((and (save-excursion
(goto-char indent-point)
(skip-syntax-forward " ")
(not (looking-at ":")))
(save-excursion
(goto-char orig-point)
(looking-at ":")))
(save-excursion
(goto-char (+ 2 (elt state 1)))
(current-column)))
(t
(let ((function (buffer-substring (point)
(progn (forward-sexp 1) (point))))
method)
(setq method (or (function-get (intern-soft function)
'lisp-indent-function)
(get (intern-soft function) 'lisp-indent-hook)))
(cond ((or (eq method 'defun)
(and (null method)
(> (length function) 3)
(string-match "\\`def" function)))
(lisp-indent-defform state indent-point))
((integerp method)
(lisp-indent-specform method state
indent-point normal-indent))
(method
(funcall method indent-point state)))))))))
:custom
(lisp-indent-function #'config-elisp--better-lisp-indent-function))Emacs lisp syntax checking
checkdoc - linting for Elisp docstrings
(use-package checkdoc
:after (:all flycheck elisp-mode)
:demand t
:custom
(checkdoc-force-docstrings-flag nil)
(checkdoc-arguments-in-order-flag nil))flycheck-package - checker for package.el conventions
(use-package flycheck-package
:after (:all flycheck elisp-mode)
:demand t
:config
(flycheck-package-setup))Disable these checkers in org src blocks
(defun disable-flycheck-checkers-for-org-src-block ()
(push 'emacs-lisp-package flycheck-disabled-checkers)
(push 'emacs-lisp-checkdoc flycheck-disabled-checkers))
(add-hook 'org-src-mode-hook 'disable-flycheck-checkers-for-org-src-block)prettify-symbols-mode - Pretty lambdas in Lisp modes
(use-package prettify-symbols-mode
:hook
(emacs-lisp-mode . prettify-symbols-mode)
(prettify-symbols-mode . prettify-symbols-setup)
:preface
(defun prettify-symbols-setup ()
(cond
((derived-mode-p 'emacs-lisp-mode 'lisp-mode 'scheme-mode)
(setq-local prettify-symbols-alist '(("lambda" . ?λ)))))))typescript-mode - TypeScript language support
typescript-mode adds a major mode with syntax highlighting for TypeScript
files.
(use-package typescript-mode
:mode
("\\.tsx?\\'" . typescript-mode)
("\\.ts\\.snap\\'" . typescript-mode)
:custom
(typescript-indent-level 2))nix-mode Configure support for the Nix language
(use-package nix-mode
:mode (("\\.nix\\'" . nix-mode)
("\\.nix.in\\'" . nix-mode))
:custom
(nix-indent-function 'nix-indent-line))Teach Emacs how to create a Nix language REPL
(use-package nix-repl
:config
(add-to-list 'display-buffer-alist
`(,(rx bos "*Nix-REPL*" eos)
(display-buffer-reuse-window display-buffer-at-bottom)
(slot . ,display-buffer-slot-repls)
(reusable-frames . visible)
(window-height . 0.4))))yaml-mode - YAML editing support
(defun disable-autofill ()
(auto-fill-mode -1))
(use-package yaml-mode
:mode ("\\.\\(e?ya?\\|ra\\)ml\\'" . yaml-mode)
:general
(:states '(normal insert) :keymaps 'yaml-mode-map
[backtab] 'yaml-indent-line)
:config
(add-hook 'yaml-mode-hook #'disable-autofill))json-mode - JSON editing support
(use-package json-mode
:commands (json-mode)
:mode ("\\.json\\'" . json-mode)
:custom
(json-reformat:indent-width 2))highlight-indent-guides - show indentation level indicators
(use-package highlight-indent-guides
:hook
(yaml-mode . highlight-indent-guides-mode)
(json-mode . highlight-indent-guides-mode))markdown-mode - Markdown file support
(use-package markdown-mode
:mode
("\\.md\\'" . gfm-mode)
("\\.markdown\\'" . markdown-mode)
:general
(:states 'normal :keymaps 'markdown-mode-map
"TAB" #'markdown-cycle
"RET" #'markdown-follow-thing-at-point)
(:keymaps 'markdown-mode-map
"C-c C-l" #'markdown-insert-link
"C-c C-i" #'markdown-insert-image
"C-c C-f" #'markdown-insert-footnote
"C-c C--" #'markdown-insert-hr
"C-c C-e" #'markdown-export
"C-c C-o" #'markdown-preview
"C-c p" #'markdown-live-preview-mode
"C-<return>" #'markdown-insert-header-dwim
"M-<left>" #'markdown-promote
"M-<right>" #'markdown-demote
"M-<up>" #'markdown-move-subtree-up
"M-<down>" #'markdown-move-subtree-down)
:custom
(markdown-asymmetric-header t)
(markdown-command "multimarkdown")
(markdown-fontify-code-blocks-natively t)
(markdown-hide-urls t))(mode-leader-set-key :keymaps '(gfm-mode-map markdown-mode-map)
"i" '(nil :wk "insert")
"i h" '(markdown-insert-header-dwim :wk "header")
"i c" '(markdown-insert-gfm-code-block :wk "code block...")
"i i" '(markdown-insert-image :wk "image")
"i f" '(markdown-insert-footnote :wk "footnote")
"i l" '(markdown-insert-link :wk "link")
"i w" '(markdown-insert-wiki-link :wk "wiki link")
"i -" '(markdown-insert-hr :wk "hr")
"m" '(nil :wk "markup")
"m b" '(markdown-insert-bold :wk "bold")
"m i" '(markdown-insert-italic :wk "italic")
"m k" '(markdown-insert-kbd :wk "kbd")
"m q" '(markdown-insert-blockquote :wk "blockquote")
"m s" '(markdown-insert-strike-through :wk "strike-through")
"o" '(markdown-preview :wk "preview")
"p" '(markdown-live-preview-mode :wk "preview (live)")
"e" '(markdown-export :wk "export"))ledger-mode - Ledger accounting software interface
(use-package ledger-mode
:mode ("\\.ledger$" . ledger-mode)
:general
(:keymaps 'ledger-report-mode-map
"C-c C-c" #'ledger-report
"q" #'kill-buffer-and-window)
(:keymaps 'ledger-mode-map
"C-c C-c" #'ledger-report
"M-RET" #'ledger-toggle-current-transaction)
:custom
(ledger-report-use-header-line nil)
(ledger-post-account-alignment-column 2)
(ledger-fontify-xact-state-overrides nil))Configure how reports are displayed
(use-package ledger-mode
:config
(add-to-list 'display-buffer-alist
`(,(rx bos "*Ledger Report*" eos)
(display-buffer-reuse-window display-buffer-pop-up-window)
(slot . ,display-buffer-slot-documents)
(reusable-frames . visible))))Highlight negative numbers in red
(use-package ledger-mode
:preface
(defface ledger-report-negative-amount
`((t (:foreground "red")))
"Face for negative amounts in ledger reports."
:group 'ledger-faces)
:config
(font-lock-add-keywords
'ledger-report-mode
`((,(rx "$" (* space) "-" (+ digit) (* (any digit ",")) (? "." (+ digit))) . 'ledger-report-negative-amount)
(,(rx (+ digit) "-" (= 3 alpha) "-" (+ digit)) . 'ledger-font-posting-date-face)))
(add-hook 'ledger-report-mode-hook 'font-lock-fontify-buffer))Changing transaction timestamps
Use C-c C-. to change the timestamp of the transaction at point.
(use-package ledger-mode
:general
(:keymaps 'ledger-mode-map
"C-c C-." #'config-ledger-set-xact-timestamp)
:preface
(defun config-ledger-set-xact-timestamp ()
(interactive)
(when-let* ((ctx (ledger-xact-context))
(value (ledger-context-field-value ctx 'date))
(start (ledger-context-field-position ctx 'date))
(end (ledger-context-field-end-position ctx 'date))
(updated (ledger-read-date "Transaction date: ")))
(if (string= value updated)
(user-error "Date unchanged")
(save-excursion
(goto-char start)
(delete-region start end)
(insert updated))
(let ((message-log-max))
(message "Date changed: %s -> %s" value updated))))))Format ledger buffer
Define a command to format a ledger buffer.
(use-package ledger-mode
:general (:keymaps 'ledger-mode-map "M-q" #'ledger-format-buffer)
:functions (ledger-mode-clean-buffer)
:preface
(defvar ledger-post-amount-alignment-column 52)
(defun ledger-format--align-price-assertion ()
(when (string-match-p (rx (+ space) "=" (* space) (not (any digit)))
(buffer-substring (line-beginning-position)
(line-end-position)))
(unwind-protect
(progn
(goto-char (line-beginning-position))
(search-forward "=")
(goto-char (match-beginning 0))
(indent-to (1+ ledger-post-amount-alignment-column))
(skip-chars-forward " =")
(just-one-space))
(goto-char (line-end-position)))))
(defun ledger-format-buffer ()
"Reformat the buffer."
(interactive "*")
(let ((pos (point)))
(ignore-errors
(ledger-mode-clean-buffer))
(goto-char (point-min))
(while (search-forward-regexp (rx (>= 2 space) "=") nil t)
(ledger-format--align-price-assertion))
(goto-char pos))))flycheck-ledger - Flycheck support for ledger buffers
(use-package flycheck-ledger
:after (:all flycheck ledger-mode)
:demand t)autctex - Tex & Latex editing
Use auctex as the Tex and Latex editing mode.
auctex is disgusting and clobbers the builtin Tex modes. To load it lazily,
intercept attempts to load Tex files and make sure auctex is loaded first.
(defun config-latex--lazy-load-auctex ()
(when (string-match-p (rx "." (or "latex" "tex") string-end)
(buffer-name))
(require 'tex-site)))
(add-hook 'find-file-hook #'config-latex--lazy-load-auctex)(use-package tex
:preface
(defvar-local TeX-syntactic-comments t)
:custom
(TeX-command (getenv "NIX_EMACS_TEX_PROGRAM"))
(TeX-auto-save t)
(TeX-parse-self t)
(TeX-source-correlate-start-server nil)
;; Use Emacs pdf-tools as viewer.
(TeX-view-program-selection '((output-pdf "PDF Tools")))
(TeX-view-program-list '(("PDF Tools" TeX-pdf-tools-sync-view))))(use-package latex
:custom
(LaTeX-command (getenv "NIX_EMACS_TEX_PROGRAM"))
;; Don't insert line-break at inline math.
(LaTeX-fill-break-at-separators nil)
:config
(add-hook 'LaTeX-mode-hook 'flyspell-mode)
(add-hook 'LaTeX-mode-hook 'TeX-fold-mode)
(add-hook 'LaTeX-mode-hook 'LaTeX-math-mode)
(add-hook 'LaTeX-mode-hook 'TeX-source-correlate-mode)
(add-hook 'LaTeX-mode-hook 'TeX-PDF-mode))Smarter autofill function
Teach the autofill function in Latex buffers not to fill in certain contexts.
(use-package latex
:functions (LaTeX-current-environment)
:config
(defvar config-latex-no-indent-envs '("equation" "equation*" "align" "align*" "tabular" "tikzpicture"))
(defun config-latex--autofill ()
;; Check whether the pointer is currently inside one of the
;; environments described in `config-latex-no-indent-envs' and if so, inhibits
;; the automatic filling of the current paragraph.
(let ((env)
(should-fill t)
(level 0))
(while (and should-fill (not (equal env "document")))
(cl-incf level)
(setq env (LaTeX-current-environment level))
(setq should-fill (not (member env config-latex-no-indent-envs))))
(when should-fill
(do-auto-fill))))
(defun config-latex--configure-autofill ()
(auto-fill-mode +1)
(setq-local auto-fill-function #'config-latex--autofill))
(add-hook 'LaTeX-mode-hook 'config-latex--configure-autofill))Build command
C-c C-c builds the current buffer with tectonic.
(use-package latex
:general
(:keymaps 'LaTeX-mode-map
"C-c C-b" #'config-latex-build)
:preface
(autoload 'TeX-command "tex-buf")
(autoload 'TeX-master-file "tex")
(autoload 'TeX-save-document "tex-buf")
(defvar TeX-save-query)
(defun config-latex-build ()
(interactive)
(progn
(let ((TeX-save-query nil))
(TeX-save-document (TeX-master-file)))
(TeX-command (getenv "NIX_EMACS_TEX_PROGRAM") 'TeX-master-file -1))))Environment folding
tex-fold enables folding of macros and environments. It’s part of auctex.
(use-package tex-fold
:after tex
:demand t)Code completion
(use-package company-auctex
:after (:all tex company)
:demand t
:config (company-auctex-init))Show preview on save
(use-package latex-preview-pane
:general (:keymaps 'LaTeX-mode-map "C-c p" #'latex-preview-pane))graphql - GraphQL schema definition language editing support
(use-package graphql-mode
:mode ("\\.graphql\\'" . graphql-mode))Teach dumb-jump about GraphQL files
(use-package graphql-mode
:after dumb-jump
:config
(add-to-list 'dumb-jump-language-file-exts '(:language "graphql" :ext "graphql" :agtype nil :rgtype nil))
(add-to-list 'dumb-jump-language-file-exts '(:language "graphql" :ext "gql" :agtype nil :rgtype nil))
(add-to-list 'dumb-jump-find-rules
'(:type "type" :supports ("ag" "grep" "rg") :language "graphql"
:regex "(input|type|union)\\s+JJJ\\b"))
(add-to-list 'dumb-jump-find-rules
'(:type "enum"
:supports ("ag" "grep" "rg") :language "graphql"
:regex "enum\\s+JJJ\\b"))
(add-to-list 'dumb-jump-find-rules
'(:type "scalar"
:supports ("ag" "grep" "rg") :language "graphql"
:regex "scalar\\s+JJJ\\b")))js - Editing support for JavaScript
(use-package js
:mode ("\\.[cm]?jsx?\\'" . js-mode)
:custom
(js-indent-level 2)
(js-switch-indent-offset 2)
(js-js-tmpdir (f-join paths-cache-directory "js")))plantuml-mode - Editing support for PlantUML diagrams
(use-package plantuml-mode
:mode
("\\.plantuml\\'" . plantuml-mode)
("\\.puml\\'" . plantuml-mode)
:general
(:keymaps 'plantuml-mode-map
"C-c C-b" 'recompile)
:custom
(plantuml-default-exec-mode 'jar)
(plantuml-indent-level 2)
(plantuml-jar-path (getenv "NIX_EMACS_PLANTUML_JAR"))
:config
(modify-syntax-entry ?_ "w" plantuml-mode-syntax-table))Flycheck support
(use-package flycheck-plantuml
:after (:all flycheck plantuml-mode)
:demand t
:config (flycheck-plantuml-setup))Highlight arrows and important keywords
(use-package plantuml-mode
:config
(defconst config-plantuml--participant-binder-rx
`(and word-start (+? (syntax word)) word-end))
(defconst config-plantuml--arrows-rx
(let* ((direction '(or "up" "u" "down" "d" "left" "l" "right" "r"))
(directives '(and "[" (*? nonl) "]"))
(lines '(+ "-"))
(dots '(+ ".")))
;; HACK: Make sure we have at least 2 characters in an arrow to avoid
;; nonsense.
`(or "---"
"..."
(and "<" ,lines)
(and "<" ,dots)
(and ,lines ">")
(and ,dots ">")
(and "<" ,lines ">")
(and "<" ,dots ">")
(and (? "<")
(or (and ,lines (? ,directives) (? ,direction) ,lines)
(and ,dots (? ,directives) (? ,direction) ,dots))
(? ">")))))
(font-lock-add-keywords
'plantuml-mode
`((,(rx bol (* space) (group (or "@startuml" "@enduml")))
(0 'font-lock-preprocessor-face))
(,(rx bol (* space) (group "title") symbol-end)
(1 'font-lock-preprocessor-face))
(,(rx bol (* space) (group "note"))
(1 'font-lock-keyword-face)
(,(rx (+ space) (group (or "left" "right" "bottom" "top") (+ space) (group "of")))
nil nil
(1 'font-lock-keyword-face)
(2 'font-lock-keyword-face))
(,(rx (+ space) (group (+ (syntax word))) eol)
nil nil
(1 'font-lock-variable-name-face)))
(,(rx bol (* space) (group "end" (+ space) "note"))
(1 'font-lock-keyword-face))
(,(rx bol (* space) (group "!include" (* word)))
(0 font-lock-keyword-face)
(,(rx (+ nonl)) nil nil (0 'font-lock-string-face)))
(,(rx bol (* space) (group "!startsub"))
(0 'font-lock-preprocessor-face)
(,(rx (+ nonl)) nil nil (0 'font-lock-function-name-face)))
(,(rx bol (* space) (group "!endsub")) (0 'font-lock-preprocessor-face))
;; Naive macro highlighting
(,(rx bol (* space) (group upper (* (syntax word))) (* space) "("
(? (group (+ (syntax word)))))
(1 'font-lock-type-face)
(2 'font-lock-variable-name-face))
;; Groupings
(,(rx bol (* space) (group (or "package" "node" "folder" "frame" "cloud" "database"))
symbol-end)
(1 'font-lock-keyword-face)
(,(rx symbol-start (group "as") (+ space) (group (+ (syntax word)) symbol-end))
nil nil
(1 'font-lock-keyword-face)
(2 'font-lock-variable-name-face)))
;; Sequence diagrams
(,(rx bol (* space) (group (or "actor" "boundary" "control"
"entity" "database" "collections"))
(? (+ space)
(group (+ (syntax word)))
symbol-end))
(1 'font-lock-keyword-face)
(2 'font-lock-variable-name-face))
;; Improved arrows syntax highlighting
(,(rx-to-string `(and bol
(* space) (group ,config-plantuml--participant-binder-rx)
(* space) (group ,config-plantuml--arrows-rx)
(* space) (group ,config-plantuml--participant-binder-rx))
t)
(1 'font-lock-variable-name-face)
(2 'font-lock-keyword-face)
(3 'font-lock-variable-name-face)
(,(rx (group ":") (* space) (group (* nonl)))
nil nil
(1 'font-lock-keyword-face)
(2 'font-lock-string-face)))
;; Creole text formatting: https://plantuml.com/creole
(,(rx (not "~") (group "**") (group (+? any)) (group "**"))
(1 'parenthesis)
(2 'bold)
(3 'parenthesis))
(,(rx (not "~") (group "//") (group (+? any)) (group "//"))
(1 'parenthesis)
(2 'italic)
(3 'parenthesis))
(,(rx (not "~") (group "\"\"") (group (+? any)) (group "\"\""))
(1 'parenthesis)
(2 'org-code)
(3 'parenthesis))
(,(rx (not "~") (group "--") (group (+? any)) (group "--"))
(1 'parenthesis)
(2 '(:strike-through t))
(3 'parenthesis))
(,(rx (not "~") (group "__") (group (+? any)) (group "__"))
(1 'parenthesis)
(2 'underline)
(3 'parenthesis))
(,(rx (not "~") (group "~~") (group (+? any)) (group "~~"))
(1 'parenthesis)
(2 '(:underline (:style wave)))
(3 'parenthesis))
(,(rx bol (* space) (group (+ "#")) (+ (not (any "*"))))
(1 'org-list-dt))
(,(rx bol (* space) (group "*") (+ (not (any "*"))))
(1 'org-list-dt)))))terraform-mode & hcl-mode - Editing support for Terraform
(use-package terraform-mode
:mode ("\\.tf\\(vars\\)?\\'" . terraform-mode))
(use-package hcl-mode
:mode
("\\.hcl\\'" . hcl-mode)
("\\.nomad\\'" . hcl-mode))Format HCL buffers on save
(use-package hcl-mode
:after format-all
:config
;; Dynamically eval to avoid macroexpansion error
(eval
'(define-format-all-formatter terragrunt-fmt
(:executable "terragrunt")
(:install (macos "brew install terragrunt"))
(:languages "Terragrunt")
(:features)
(:format (format-all--buffer-easy executable "fmt" "-no-color" "-"))))
(add-to-list 'format-all-default-formatters 'terragrunt-fmt))css-mode - CSS editing support
(use-package css-mode
:defer t
:custom
(css-indent-offset 2))hexl-mode - Hex editing
hexl is Emacs’ built-in hex editor.
(use-package hexl
:general
(:states 'motion :keymaps 'hexl-mode-map
"]]" #'hexl-end-of-1k-page
"[[" #'hexl-beginning-of-1k-page
"h" #'hexl-backward-char
"l" #'hexl-forward-char
"j" #'hexl-next-line
"k" #'hexl-previous-line
"$" #'hexl-end-of-line
"^" #'hexl-beginning-of-line
"0" #'hexl-beginning-of-line))conf-mode - Configuration files
Configure conf-mode for use with more kinds of config files.
(use-package conf-mode
:mode
("\\.env\\.erb\\'" . conf-mode)
("\\.conf\\.erb\\'" . conf-mode)
("\\.kll\\'" . conf-mode))dockerfile-mode - editing support for Dockerfile
(use-package dockerfile-mode
:mode ("Dockerfile\\'" . dockerfile-mode))Applications
pass - Frontend to Unix password-store
(use-package pass
:commands (pass)
:after (password-store)
:general
(:states '(normal) :keymaps 'pass-view-mode-map "q" #'kill-this-buffer)
(:states '(normal) :keymaps 'pass-mode-map
"u" #'pass-copy-username
"U" #'pass-copy-url
"J" #'pass-goto-entry
"y" #'pass-copy-password
"f" #'pass-copy-field
"q" #'kill-this-buffer)
:custom
(password-store-password-length 50)
(pass-username-field "email")
:init
(add-to-list 'display-buffer-alist
`(,(rx bos "*Password-Store*" eos)
(display-buffer-reuse-window display-buffer-fullframe)
(reusable-frames . visible))))pdf-tools - PDF reader
(use-package pdf-tools
:mode ("\\.[pP][dD][fF]\\'" . pdf-view-mode)
:general (:states '(motion normal) :keymaps 'pdf-view-mode-map
"t" #'pdf-view-midnight-minor-mode
"n" #'pdf-view-next-page
"N" #'pdf-view-previous-page
"p" #'pdf-view-previous-page)
:hook
(pdf-view-mode . pdf-view-midnight-minor-mode)
:custom
(pdf-view-display-size 'fit-page)
:init
(use-package pdf-history
:commands (pdf-history-minor-mode))
(use-package pdf-occur
:commands (pdf-occur-global-minor-mode))
:config
(require 'pdf-annot)
(require 'pdf-sync)
(require 'pdf-links)
(require 'pdf-outline)
(require 'pdf-history)
(require 'pdf-cache)
(require 'pdf-view)
;; Redefine a few macros as functions to work around byte compilation errors.
(defun pdf-view-current-page (&optional window)
(image-mode-window-get 'page window))
(defun pdf-view-current-overlay (&optional window)
(image-mode-window-get 'overlay window))
(pdf-tools-install))mu4e - Mail user agent
Use the system’s mu4e installation for mail management. I use the
systemd/launchd to run mbsync and indexing, so mu4e doesn’t trigger fetching
itself.
(add-to-list 'load-path (getenv "NIX_EMACS_MU_LISP_DIR"))
(use-package mu4e
:commands (mu4e mu4e-compose-new)
:custom
(mu4e-mu-binary (getenv "NIX_EMACS_MU_BINARY"))
(mu4e-bookmarks '(("flag:unread AND NOT (flag:trashed OR m:/walrus/Archive)"
"Unread messages" ?u)
("d:today..now"
"Today's messages" ?t)
("d:7d..now"
"Last 7 days" ?w)
("d:30d..now"
"Last 30 days" ?m)
("m:/walrus/Inbox"
"Inbox" ?i)
("m:/walrus/Notifications AND d:14d..now"
"Notifications" ?n)
("m:/walrus/Sent"
"Sent messages" ?s)
("github"
"Code & PRs" ?c)))
(mu4e-attachment-dir (f-expand "~/Downloads"))
(mu4e-context-policy 'pick-first)
(mu4e-compose-context-policy 'ask-if-none)
(message-kill-buffer-on-exit t)
(mu4e-view-use-gnus t)
(mu4e-use-fancy-chars t)
(mu4e-headers-include-related nil)
(mu4e-headers-attach-mark '("a" . "A"))
(mu4e-headers-unread-mark '("u" . "●"))
(mu4e-headers-seen-mark '(" " . " "))
(mu4e-hide-index-messages t)
(mu4e-headers-skip-duplicates t)
(mu4e-index-lazy-check t)
(mu4e-confirm-quit t)
(mu4e-view-prefer-html t)
(mu4e-view-show-images t)
(mu4e-view-show-addresses t)
(mu4e-headers-date-format "%d-%m-%y %k:%M")
(mu4e-completing-read-function #'completing-read)
(sendmail-program "msmtp")
(message-send-mail-function #'message-send-mail-with-sendmail)
(mu4e-change-filenames-when-moving t)
;; Update every 30 seconds.
(mu4e-update-interval 30)
;; Ensure I'm never prompted for the buffer coding system when sending mail.
(sendmail-coding-system 'utf-8)
;; Send email with long lines and format=flowed.
(mu4e-compose-format-flowed t)
(fill-flowed-encode-column 998)
;; Custom rendering of HTML messages
(mu4e-html2text-command #'config-mail--shr-buffer))Always use mu4e for mail composition
(global-set-key [remap compose-mail] #'mu4e-compose-new)Display mu4e fullscreen
(use-package mu4e
:config
(add-to-list 'display-buffer-alist
`(,(rx bos " *mu4e-main*" eos)
(display-buffer-reuse-window
display-buffer-fullframe)
(reusable-frames . visible))))Wrap lines in an intuitive way while viewing and editing.
(use-package mu4e
:config
;; Use word wrap instead of auto-fill.
(add-hook 'mu4e-compose-mode-hook #'turn-off-auto-fill)
(add-hook 'mu4e-compose-mode-hook (lambda () (setq word-wrap t)))
;; Wrap lines when viewing.
(add-hook 'mu4e-view-mode-hook #'visual-line-mode))Define commands for viewing messages in a browser
Either with eww or the default system browser.
(use-package mu4e
:config
;; View html message in eww. `av` in view to activate
(add-to-list 'mu4e-view-actions '("ViewInBrowser" . mu4e-action-view-in-browser) t)
;; View html message in external browser. `a&` in view to activate
(add-to-list 'mu4e-view-actions '("&viewInExternalBrowser" . config-mail--view-in-external-browser-action) t))Define a smart refile action for r
Marks messages as ‘read’ and moves them into the appropriate folder.
(use-package mu4e
:config
(defun config-mail--message-from-me-p (msg)
(equal (mu4e-get-sent-folder msg) (mu4e-message-field msg :maildir)))
(setf (alist-get 'refile mu4e-marks)
'(:char ("r" . "▶")
:prompt "refile"
:dyn-target (lambda (target msg)
(if (config-mail--message-from-me-p msg)
(mu4e-get-sent-folder msg)
(mu4e-get-refile-folder msg)))
:action (lambda (docid msg target)
(unless (config-mail--message-from-me-p msg)
(mu4e~proc-move docid (mu4e~mark-check-target target) "+S-u-N"))))))Insert signatures before quoted content, as in other mail user agents
(use-package mu4e
:custom
(message-forward-before-signature nil)
(message-citation-line-function #'message-insert-formatted-citation-line)
(message-citation-line-format "On %a, %b %d %Y, %f wrote:\n")
:config
(defun mu4e--insert-signature-before-quoted-message ()
(unless (member mu4e-compose-type '(edit resend))
(save-excursion
(save-restriction
(widen)
(cond
((eq mu4e-compose-type 'new)
(message-goto-body)
(kill-region (point) (point-max)))
((message-goto-signature)
(forward-line -2)
(delete-region (point) (point-max))))
(message-goto-body)
(insert "\n")
(narrow-to-region (point-min) (point))
(let ((message-signature t)
(mu4e-compose-signature t)
(mu4e-compose-signature-auto-include t))
(message-insert-signature))
(when (member mu4e-compose-type '(forward reply))
(goto-char (point-max))
(insert "\n"))))))
(add-hook 'mu4e-compose-mode-hook #'mu4e--insert-signature-before-quoted-message))Don’t quit mu4e, just bury the buffer
(global-set-key [remap mu4e-quit] #'bury-buffer)Teach org-mode how to store links to messages in mu4e
(use-package org-mu4e
:after (:any org mu4e))Display newline symbols in the buffer when a hard newline would be used
Normally newlines are reflowed automatically.
(use-package messages-are-flowing
:commands (messages-are-flowing--mark-hard-newlines)
:init
(defun setup-hard-newlines ()
(use-hard-newlines nil 'always)
(add-hook 'after-change-functions 'messages-are-flowing--mark-hard-newlines nil t))
:hook (message-mode . setup-hard-newlines))org-mode - Org document format & editing support
Set general org-mode variables
(use-package org
:custom
(org-directory paths-org-directory)
(org-default-notes-file (f-join paths-org-directory "notes.org"))
(org-bookmark-names-plist nil)
(org-imenu-depth 4)
(org-indirect-buffer-display 'current-window)
(org-link-elisp-confirm-function 'y-or-n-p)
(org-image-actual-width nil)
(org-return-follows-link t)
:config
(add-hook 'org-mode-hook #'auto-revert-mode)
(add-hook 'org-mode-hook #'visual-line-mode)
:general
("C-c a" 'org-agenda
"C-c s" 'org-search-view
"C-c t" 'org-todo-list
"C-c /" 'org-tags-view)
(:states '(insert) :keymaps 'org-mode-map
"<tab>" (general-predicate-dispatch 'org-cycle
(eq ?: (char-before)) 'emojify-insert-emoji
(yas--templates-for-key-at-point) 'yas-expand))
(:states '(emacs normal) :keymaps 'org-mode-map
"<backtab>" 'org-global-cycle
"<tab>" 'org-cycle
"C-c c" 'org-columns
"M-n" 'org-metadown
"M-p" 'org-metaup
"RET" 'org-open-at-point)
(:states '(normal motion insert emacs) :keymaps 'org-mode-map
"C-c C-." 'org-time-stamp-inactive
"C-c ." 'org-time-stamp
"C-c o" 'org-table-toggle-coordinate-overlays))Set up major-mode leader keybindings
(mode-leader-set-key :keymaps 'org-mode-map
"A" '(org-archive-subtree :wk "archive")
"/" '(org-sparse-tree :wk "sparse tree...")
"r" (list (general-predicate-dispatch 'org-refile
(bound-and-true-p org-capture-mode) 'org-capture-refile)
:wk "refile")
"u" '(nil :wk "org-roam-ui")
"u f" '(org-roam-ui-follow-mode :wk "toggle follow")
"u o" '(orui-open :wk "open in browser")
"u z" '(orui-node-zoom :wk "zoom")
"u l" '(orui-node-local :wk "local")
"c" '(org-clock-display :wk "clock overlays")
"i" '(org-id-get-create :wk "get/create heading ID")
"x" '(org-cut-subtree :wk "cut")
"y" '(org-copy-subtree :wk "copy")
"p" '(org-paste-subtree :wk "past")
"n" '(org-num-mode :wk "show heading numbers")
"o" '(org-tree-to-indirect-buffer :wk "tree to indirect buf")
"t" '(org-show-todo-tree :wk "todo tree")
"e" '(nil :wk "babel")
"e b" '(org-babel-execute-buffer :wk "execute buffer")
"e c" '(org-babel-tangle-clean :wk "clean")
"e e" (list (general-predicate-dispatch 'org-babel-execute-subtree
(org-in-src-block-p) 'org-babel-execute-src-block)
:wk "execute")
"e i" '(org-babel-view-src-block-info :wk "src info")
"e x" '(org-babel-demarcate-block :wk "split block")
"e v" '(org-babel-mark-block :wk "mark block"))Allow ID lookups to work in task files
(use-package org-id
:config
(dolist (file (f-files (f-join paths-org-directory "tasks")))
(add-to-list 'org-id-files file)))
(use-package org-id
:after org-roam
:config
(dolist (file (f-files org-roam-directory nil t))
(add-to-list 'org-id-files file)))org-funcs - org functions used throughout config
(use-package org-funcs
:commands
org-funcs-agenda-dwim
org-funcs-todo-list
org-funcs-read-url
org-funcs-url-to-reference
org-funcs-toggle-priority
:general
(:keymaps 'org-mode-map :states '(normal insert emacs)
"C-c l" 'org-funcs-insert-url-as-link))evil-org - Improve Evil integration with org-mode
(use-package evil-org
:hook (org-mode . evil-org-mode)
:custom
(evil-org-key-theme '(additional
return
calendar
navigation
textobjects
todo)))org-capture - Customise basic capture templates
(use-package org-capture
:after org-funcs
:config
(org-funcs-update-capture-templates '(("c" "Clocking")
("cn" "Note for currently clocked heading"
plain
(clock)
(function org-funcs-capture-note-to-clocked-heading)
:immediate-finish t))))Evil state improvements
Automatically enter insert state when inserting new headings or using
org-capture.
(use-package org
:after evil
:config
(defun org-enter-evil-insert-state-for-capture (&rest _)
(when (and (called-interactively-p nil)
(bound-and-true-p org-capture-mode))
(evil-insert-state)))
(defun org-enter-evil-insert-state (&rest _)
(when (called-interactively-p nil)
(evil-insert-state)))
(add-hook 'org-log-buffer-setup-hook #'evil-insert-state)
(advice-add 'org-capture :after #'org-enter-evil-insert-state-for-capture)
(advice-add 'org-insert-heading :after #'org-enter-evil-insert-state)
(advice-add 'org-insert-heading-respect-content :after #'org-enter-evil-insert-state)
(advice-add 'org-insert-todo-heading-respect-content :after #'org-enter-evil-insert-state)
(advice-add 'org-insert-todo-heading :after #'org-enter-evil-insert-state))Visual settings
(use-package org
:custom
(org-cycle-separator-lines 0)
(org-hide-emphasis-markers nil)
(org-startup-indented t)
(org-startup-with-latex-preview t)
(org-startup-folded 'content)
(org-startup-shrink-all-tables t)
(org-startup-with-inline-images t))Use bullet characters for list items
(use-package org-mode
:config
(font-lock-add-keywords 'org-mode
`((,(rx bol (* space) (group "-") (+ space))
(0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•")))))))org-bullets - Use pretty UTF-8 bullet characters for headlines
(use-package org-superstar
:hook (org-mode . org-superstar-mode)
:custom
(org-superstar-headline-bullets-list '("○"))
(org-indent-mode-turns-on-hiding-stars nil)
(org-superstar-leading-bullet ?\s)
:config
(setf (alist-get 45 org-superstar-item-bullet-alist) ?•))Attachments
(use-package org
:custom
(org-attach-id-dir (f-join paths-org-directory "data")))Automatically commit attachments to git.
(use-package org-attach
:demand t
:after org
:config (require 'org-attach-git)
:custom
;; This needs to be set for attach commands to work as expected, independently
;; of `org-use-property-inheritance'.
(org-attach-use-inheritance t))Automatically save buffers
Save org buffers automatically after certain operations.
(use-package org
:config
(defun org-ad-save-buffers (&rest _)
(org-save-all-org-buffers))
(advice-add 'org-archive-subtree :after #'org-ad-save-buffers)
(advice-add 'org-refile :after #'org-ad-save-buffers))Document formatting style
(use-package org
:custom
(org-M-RET-may-split-line nil)
(org-adapt-indentation nil)
(org-blank-before-new-entry '((heading . t) (plain-list-item . auto)))
(org-catch-invisible-edits 'smart)
(org-footnote-auto-adjust t)
(org-insert-heading-respect-content t)
(org-loop-over-headlines-in-active-region 'start-level))org-format-headings provides some commands to clean up the whitespace around
org headings.
(use-package org-format-headings
:commands (org-format-all-headings org-format-heading))Load org-export packages
(use-package ox-gfm
:after org)
(use-package ox-slack
:after org)
(use-package htmlize
:defer t)Todos, checkboxes and statistics
(use-package org
:custom
(org-checkbox-hierarchical-statistics t)
(org-checkbox-hierarchical-statistics t)
(org-enforce-todo-dependencies t)
(org-hierarchical-todo-statistics nil)
(org-todo-keywords '((type "TODO(t)" "WAIT(w)" "|" "DONE(d)" "CANCELLED(c@)"))))Completing all child TODOs will change the parent TODO to DONE.
(use-package org
:config
(defun org-set-done-when-all-children-completed (_n-done n-todo)
(let (org-log-done org-log-states) ; turn off logging
(org-todo (if (zerop n-todo) "DONE" "TODO"))))
(add-hook 'org-after-todo-statistics-hook #'org-set-done-when-all-children-completed))Refiling
(use-package org
:custom
(org-outline-path-complete-in-steps nil)
(org-refile-allow-creating-parent-nodes 'confirm)
(org-refile-use-outline-path 'file))Limit refile depth and restrict to specific candidates.
(use-package org
:config
(defun org-funcs-refile-candidates ()
(f-files (f-join org-directory "tasks")
(lambda (it)
(f-ext-p it "org"))))
(defun org-funcs-refile-verify-function ()
(let ((keyword (nth 2 (org-heading-components))))
(not (member keyword org-done-keywords))))
:custom
(org-refile-targets '((nil . (:maxlevel . 10)) (org-funcs-refile-candidates . (:maxlevel . 3))))
(org-refile-target-verify-function 'org-funcs-refile-verify-function))Automatically remove deleted files from agenda
(use-package org
:config
(defun config-org--always-remove-missing-agenda-files (file)
(unless (file-exists-p file)
(org-remove-file file)
(throw 'nextfile t)))
(advice-add 'org-check-agenda-file :override #'config-org--always-remove-missing-agenda-files))Logging & clocking
(use-package org
:custom
(org-clock-history-length 20)
(org-clock-in-resume t)
(org-clock-into-drawer t)
(org-clock-out-remove-zero-time-clocks t)
(org-clock-persist t)
(org-clock-persist-query-resume nil)
(org-clock-report-include-clocking-task t)
(org-clock-mode-line-total 'today)
(org-log-done 'time)
(org-log-into-drawer t)
(org-log-redeadline 'time)
(org-log-repeat 'time)
(org-log-reschedule 'time)
(org-reverse-note-order nil)
:config
(org-clock-persistence-insinuate))Exit minibuffer before adding notes.
(use-package org
:config
(defun org-ad-exit-minibuffer (&rest _)
(when (minibufferp (window-buffer (selected-window)))
(other-window 1)))
(advice-add 'org-add-log-note :before #'org-ad-exit-minibuffer))org-archive - Archiving support for org-mode content
(use-package org
:custom
(org-archive-location (concat (f-join org-directory "archive.org") "::datetree/"))
(org-archive-subtree-add-inherited-tags t))Apply either an @work or @personal context when archiving.
(use-package org
:config
(defun org-apply-tags-before-archive (&rest _)
;; Ensure we have a context before archiving.
(when (seq-empty-p (org-get-tags))
(pcase-exhaustive (read-char-choice "Set context: [w]ork [p]ersonal" '(?w ?p))
(?w (org-toggle-tag (timekeep-work-tag) 'on))
(?p nil))))
(advice-add 'org-archive-subtree :before 'org-apply-tags-before-archive))Turn priorities into an on/off flag
I customise priorities so that headlines are either ‘prioritised’ an show a cookie or ‘cleared’, which indicates no special priority.
(use-package org
:custom
(org-priority-start-cycle-with-default t)
(org-highest-priority ?A)
(org-lowest-priority ?B)
(org-default-priority ?B))C-c p toggles a priority cookie on the current heading.
Toggle priority in org-mode
(use-package org
:general
(:states 'normal :keymaps 'org-mode-map "C-c p" 'org-funcs-toggle-priority)
:config
(defun org-funcs-toggle-priority ()
"Toggle the priority cookie on the current line."
(interactive)
(save-excursion
(org-back-to-heading t)
(-let [(_ _ _ priority) (org-heading-components)]
(cond (priority
(org-priority ?\s)
(message "Priority cleared"))
(t
(org-priority ?A)
(message "Priority set")))))))Toggle priority in org-agenda
(use-package org-agenda
:general
(:states 'motion :keymaps 'org-agenda-mode-map "C-c p" 'org-funcs-agenda-toggle-priority)
:config
(defun org-funcs-agenda-toggle-priority ()
"Toggle the priority cookie on the current line."
(interactive)
(org-agenda-check-no-diary)
(unless (org-get-at-bol 'org-marker)
(org-agenda-error))
(let* ((col (current-column))
(heading-marker (org-get-at-bol 'org-hd-marker))
(buffer (marker-buffer heading-marker))
(pos (marker-position heading-marker))
(inhibit-read-only t)
updated-heading)
(org-with-remote-undo buffer
(with-current-buffer buffer
(widen)
(goto-char pos)
(org-show-context 'agenda)
(org-funcs-toggle-priority)
(setq updated-heading (org-get-heading)))
(org-agenda-change-all-lines updated-heading heading-marker)
(org-move-to-column col)))))Export
(use-package org
:custom
(org-export-backends '(ascii html latex odt slack gfm))
(org-html-html5-fancy t)
(org-html-postamble nil)
(org-export-exclude-tags '("noexport" "ignore"))
(org-export-coding-system 'utf-8))org-babel & org-src - Source code support and code blocks
(use-package org
:custom
(org-src-fontify-natively t)
(org-src-window-setup 'current-window))Defer language loading as late as possible
(use-package ob
:custom
(org-confirm-babel-evaluate nil)
(org-babel-load-languages '((emacs-lisp . t)
(sql . t)
(python . t)
(calc . t)
(shell . t)))
(org-babel-python-command (executable-find "python3")))Show images in outputs (useful for GNUplot, etc).
(use-package org
:config
(add-hook 'org-babel-after-execute-hook 'org-display-inline-images t))Prevent trailing whitespace from being created in src blocks.
(use-package org
:config
(defun org-ad-suppress-final-newline ()
(setq-local require-final-newline nil))
(defun org-ad-delete-trailing-space-on-src-block-exit (&rest _)
(delete-trailing-whitespace))
(add-hook 'org-src-mode-hook 'org-ad-suppress-final-newline)
(advice-add 'org-edit-src-exit :before 'org-ad-delete-trailing-space-on-src-block-exit))Latex
Use Tectonic as org-mode’s primary LaTeX compiler
(use-package org
:custom
(org-latex-compiler (getenv "NIX_EMACS_TEX_PROGRAM"))
(org-latex-compilers (list (getenv "NIX_EMACS_TEX_PROGRAM")))
(org-latex-pdf-process (list (concat (getenv "NIX_EMACS_TEX_PROGRAM") " -Z shell-escape --outdir=%o %f"))))Make latex previews look good on high-DPI screens
(use-package org
:custom
(org-latex-create-formula-image-program 'dvisvgm)
:config
(plist-put org-format-latex-options :scale 1.7))Make C-c C-c toggle LaTeX fragment preview for editing
(use-package org
:config
(defun config-org-at-latex-fragment-p ()
(let ((datum (org-element-context)))
(and (memq (org-element-type datum) '(latex-environment latex-fragment)))))
(defun config-org-at-latex-preview-p ()
(seq-find
(lambda (ov)
(eq (overlay-get ov 'org-overlay-type) 'org-latex-overlay))
(overlays-at (point))))
(defun config-org-maybe-toggle-latex-fragment ()
(when (or (config-org-at-latex-preview-p) (config-org-at-latex-fragment-p))
(org-latex-preview)))
(add-hook 'org-ctrl-c-ctrl-c-hook #'config-org-maybe-toggle-latex-fragment))Set some default LaTeX packages
(use-package org
:custom
(org-highlight-latex-and-related '(native script entities))
:config
(add-to-list 'org-latex-default-packages-alist '("colorlinks=true" "hyperref" nil)))Don’t apply org-block face to latex snippets
(use-package org-src
:after org
:demand t
:config
(add-to-list 'org-src-block-faces '("latex" (:inherit default :extend t))))Make latex fragments adapt to colour theme changes
(use-package org-latex-themed-previews
:after org
:demand t
:custom
(org-preview-latex-image-directory (f-join paths-cache-directory "org" "latex-previews"))
:config
(org-latex-themed-previews-mode +1))org-fragtog - Automatically toggle fragment at point
(use-package org-fragtog
:hook (org-mode . org-fragtog-mode))org-crypt - Encrypted file content
(use-package org-crypt
:custom
(org-tags-exclude-from-inheritance '("crypt"))
(org-crypt-disable-auto-save 'encypt)
:config
(with-eval-after-load 'ox
(push "crypt" org-export-exclude-tags))
(org-crypt-use-before-save-magic))Define context-sensitive key commands
(use-package org
:general
(:states '(normal insert) :keymaps 'org-mode-map
"C-c C-k"
(general-predicate-dispatch 'org-cut-subtree
(bound-and-true-p org-capture-mode) 'org-capture-kill
(string-prefix-p "*Org" (buffer-name)) 'org-kill-note-or-show-branches)
"C-c RET"
(general-predicate-dispatch 'org-insert-todo-heading
(org-at-table-p) 'org-table-hline-and-move)))Support mixed text direction in documents
Allow each paragraph to have a different text direction. This is useful for mixed-direction documents which include English with Arabic, Farsi, etc.
(use-package org
:config
(defun org-set-local-vars-and-hooks ()
(setq-local bidi-paragraph-direction nil))
(add-hook 'org-mode-hook #'org-set-local-vars-and-hooks))Always open directories in Emacs via dired
Ensure we use dired rather than the Finder on macOS.
(use-package org
:config
(add-to-list 'org-file-apps '(directory . emacs)))org-agenda - Calendaring and configurable todo lists
(use-package org-agenda
:after org
:custom
(org-agenda-files (f-join org-directory "org-agenda-files"))
(org-stuck-projects '("" nil nil ""))
(org-agenda-todo-ignore-scheduled t)
(org-agenda-hide-tags-regexp (rx (or "noexport" "someday")))
(org-agenda-include-diary nil)
(org-agenda-insert-diary-extract-time t)
(org-agenda-search-view-always-boolean t)
(org-agenda-show-all-dates nil)
(org-agenda-show-inherited-tags nil)
(org-agenda-skip-deadline-if-done t)
(org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled)
(org-agenda-skip-scheduled-if-done t)
(org-agenda-span 'day)
(org-agenda-start-on-weekday nil)
(org-agenda-window-setup 'only-window)
(org-agenda-dim-blocked-tasks 'invisible)
(org-agenda-sorting-strategy '((agenda time-up category-up priority-down todo-state-up)
(todo priority-down category-up scheduled-up)
(tags priority-down category-up)
(search category-up)))
(org-agenda-clockreport-parameter-plist '(:link t :maxlevel 3 :fileskip0 t))
(org-agenda-tags-column -100)
(org-agenda-text-search-extra-files (list (f-join org-directory "archive.org")))
(org-agenda-use-time-grid nil)
:general
(:keymaps 'org-agenda-mode-map :states 'motion
"/" 'org-agenda-filter
"?" 'org-agenda-filter-by-tag
"B" 'org-agenda-bulk-action
"v" 'org-agenda-view-mode-dispatch
"t" 'org-agenda-todo
"J" 'org-agenda-goto-date
"j" 'org-agenda-next-line
"k" 'org-agenda-previous-line
"f" 'org-agenda-later
"b" 'org-agenda-earlier
"M-j" 'org-agenda-next-item
"M-k" 'org-agenda-previous-item
"M-h" 'org-agenda-earlier
"M-l" 'org-agenda-later
"gd" 'org-agenda-toggle-time-grid
"gr" 'org-agenda-redo
"M-RET" 'org-agenda-show-and-scroll-up
"C-f" 'evil-scroll-page-down
"C-b" 'evil-scroll-page-up
[remap save-buffer] 'org-save-all-org-buffers
;; Restore bindings for search buffers
"+" 'org-agenda-manipulate-query-add
"`" 'org-agenda-manipulate-query-add-re
"-" 'org-agenda-manipulate-query-subtract
"_" 'org-agenda-manipulate-query-subtract-re))(mode-leader-set-key :keymaps 'org-agenda-mode-map
"a" '(org-agenda-archive :wk "archive")
"r" '(org-agenda-refile :wk "refile"))Define agenda views
(use-package org-agenda
:custom
(org-agenda-start-with-log-mode '(closed state))
(org-agenda-span 'day)
(org-agenda-show-future-repeats nil)
(org-agenda-ignore-properties '(effort appt))
(org-agenda-clock-consistency-checks '(:gap-ok-around ("12:20" "12:40" "4:00")
:max-duration "10:00"
:min-duration 0
:max-gap 0))
(org-agenda-custom-commands
(let ((views '((tags-todo "TODO=\"TODO\"|+PRIORITY=\"A\""
((org-agenda-overriding-header "Next Actions")
(org-agenda-skip-function #'org-funcs-skip-items-already-in-agenda)))
(todo "WAIT"
((org-agenda-overriding-header "Delegated")
(org-agenda-skip-function #'org-funcs-skip-item-if-timestamp)))
(agenda ""
((org-agenda-overriding-header "Today")
(org-agenda-use-time-grid t)))
(todo "TODO"
((org-agenda-overriding-header "Stuck Projects")
(org-agenda-skip-function #'org-project-skip-non-stuck-projects))))))
`(("p" "personal agenda" ,views
((org-agenda-tag-filter-preset '("-someday" "-ignore" "-work"))
(org-agenda-archives-mode t)))
("w" "work agenda" ,views
((org-agenda-clockreport-mode t)
(org-agenda-tag-filter-preset (list "-someday" "-ignore" (format "+%s" (timekeep-work-tag))))
(org-agenda-archives-mode t)))))))Easily exclude on-hold tasks
(use-package org-agenda
:config
(defun org-funcs-exclude-tasks-on-hold (tag)
(and (equal tag "hold") (concat "-" tag)))
:custom
(org-agenda-auto-exclude-function #'org-funcs-exclude-tasks-on-hold))Reveal context around item on TAB
(use-package org-agenda
:config
(defun config-org--on-show-item-from-agenda ()
(org-overview)
(org-reveal)
(org-show-subtree)
(org-display-outline-path))
(add-hook 'org-agenda-after-show-hook #'config-org--on-show-item-from-agenda))Integration with builtin appt (appointments)
(use-package org-agenda
:config
(add-hook 'org-finalize-agenda-hook 'org-agenda-to-appt))
(use-package appt
:custom
(appt-message-warning-time 60)
(appt-display-interval 5))Use page-break-lines to draw separator
(use-package org-agenda
:demand t
:after page-break-lines
:custom
(org-agenda-block-separator (char-to-string ?\f))
:config
(defun config-org--draw-separator (&rest _)
(page-break-lines--update-display-tables))
(advice-add 'org-agenda :after #'config-org--draw-separator)
(advice-add 'org-agenda-redo :after #'config-org--draw-separator))Make priority inherited
(defun config-org-inherited-priority (s)
(save-match-data
(cond
((string-match org-priority-regexp s)
(* 1000 (- org-priority-lowest
(org-priority-to-value (match-string 2 s)))))
((not (org-up-heading-safe))
(* 1000 (- org-priority-lowest org-priority-default)))
(t
(config-org-inherited-priority (org-get-heading))))))
(use-package org-agenda
:custom
(org-priority-get-priority-function #'config-org-inherited-priority))org-roam - Personal wiki & research tooling
(use-package org-roam
:commands
org-roam-node-find
org-roam-dailies-goto-today
org-roam-dailies-capture-today
org-roam-dailies-goto-yesterday
org-roam-dailies-goto-date
org-roam-node-create
:custom
(org-roam-directory (f-join paths-org-directory "roam/"))
(org-roam-v2-ack t)
(org-roam-verbose nil)
(org-roam-extract-new-file-path "%<%Y-%m-%d--%H-%M-%S>.org")
(org-roam-capture-templates
'(("d" "default" plain "%?" :if-new
(file+head "%<%Y-%m-%d--%H-%M-%S>.org" "#+title: ${title}\n")
:immediate-finish t
:unnarrowed t)))
(org-roam-dailies-capture-templates
'(("d" "default" entry "* %?" :target
(file+head "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n#+filetags: dailies\n"))))
:config
(org-roam-setup)
:general
(:states '(insert normal) :keymap 'org-mode-map
"C-c i" '(org-roam-node-insert :wk "insert org-roam link")
"C-c TAB" (general-predicate-dispatch 'org-roam-node-insert
(org-at-table-p) 'org-ctrl-c-tab)))Set leader keys
(mode-leader-set-key :keymaps 'org-mode-map
"<tab>" '(org-roam-buffer-toggle :wk "backlinks")
"<" '(org-roam-dailies-find-previous-note :wk "prev daily")
">" '(org-roam-dailies-find-next-note :wk "next daily")
"J" '(org-roam-dailies-goto-date :wk "goto daily...")
"E" '(org-funcs-roam-extract-subtree :wk "extract subtree to roam file...")
"l" '(nil :wk "alias")
"l a" '(org-roam-alias-add :wk "add alias")
"l x" '(org-roam-alias-remove :wk "remove alias")
"k" '(nil :wk "tags")
"k a" '(org-roam-tag-add :wk "add tag")
"k x" '(org-roam-tag-remove :wk "remove tag"))Open my index roam file as the initial buffer
(autoload 'org-roam-dailies-goto-today "org-roam-dailies")
(defun initial-buffers ()
(interactive)
(let ((inhibit-redisplay t))
(org-roam-node-visit (org-roam-node-from-id org-roam-index-node-id))
(delete-other-windows)
(goto-char (point-min))
(current-buffer)))
(setq initial-buffer-choice #'initial-buffers)Define shared paths used across packages
(defvar config-org-roam-bibliography-notes-directory (f-join paths-org-directory "roam" "notes"))
(defconst config-org-roam--default-heading-title "TODO Link literature notes to zettel in org-roam")
(defconst config-org-roam--notes-file-template (string-trim-left (format "
:PROPERTIES:
:ID: %%(org-id-get-create t)
:END:
#+title: ${title}
#+roam_key: cite:${=key=}
#+category: ${=key=}
* %s
:PROPERTIES:
:CUSTOM_ID: ${=key=}
:NOTER_DOCUMENT: %%(f-relative (orb-process-file-field \"${=key=}\") org-directory)
:END:
" config-org-roam--default-heading-title)))(make-directory config-org-roam-bibliography-notes-directory t)Automatically clean up empty dailies
(use-package org-roam-gc
:after org-roam
:demand t
:config
(run-with-idle-timer 2 nil 'org-roam-gc))Customise how the org-roam buffer is displayed
(add-to-list 'display-buffer-alist
'(("\\*org-roam\\*"
(display-buffer-in-direction)
(direction . right)
(window-width . 0.33)
(window-height . fit-window-to-buffer))))Allow lookups to roam nodes from non-roam org files
(use-package org-roam
:after org-id
:config
(dolist (node (org-roam-node-list))
(org-id-add-location (org-roam-node-id node)
(org-roam-node-file node))))bibtex - maintain a bibliography
(use-package bibtex
:custom
(bibtex-dialect 'biblatex)
(bibtex-completion-library-path (f-join org-directory "pdfs/")))bibtex-completion -
(use-package bibtex-completion
:custom
(bibtex-completion-notes-path config-org-roam-bibliography-notes-directory)
(bibtex-completion-bibliography (f-join paths-org-directory "bibliography.bib"))
(bibtex-completion-pdf-field "file")
(bibtex-completion-notes-template-multiple-files config-org-roam--notes-file-template))helm-bibtex - UI for searching the bibtex bibliography
(use-package helm-bibtex
:commands
helm-bibtex
helm-bibtex-with-local-bibliography
helm-bibtex-with-notes)org-roam-bibtex - integrate org-roam with bibtex for bibliography management
(use-package org-roam-bibtex
:hook (org-roam-mode . org-roam-bibtex-mode)
:custom
(org-roam-bibtex-preformat-keywords '("=key=" "title" "url" "file" "keywords"))
(orb-note-actions-frontend 'hydra)
(orb-templates
`(("r" "ref" plain (function org-roam-capture--get-point)
""
:file-name "${slug}"
:head ,config-org-roam--notes-file-template
:unnarrowed t)))
:init
(use-package orb-note-actions
:after org-roam-bibtex
:general (:keymaps 'org-mode-map "C-c n a" 'orb-note-actions)))org-noter - annotate PDFs etc to the an org file
I use a custom fork of org-noter that provides extra hooks to customise the
buffer it creates. This is the final piece needed to make literature notes
buffers the same, no matter how I create them.
(use-package org-noter
:after (:any org pdf-view)
:config
(defun org-noter-kill-pdf-and-session ()
(interactive)
(if (< 1 (length (window-list)))
(delete-window)
(bury-buffer))
(org-noter--with-valid-session (org-noter-kill-session session)))
:general
(:keymaps 'pdf-view-mode-map :states '(normal visual motion)
"q" 'org-noter-kill-pdf-and-session
"i" 'org-noter-insert-note
[?\t] 'org-noter)
:custom
((org-noter-always-create-frame nil)
(org-noter-root-headline-format-function `(lambda (_) ,config-org-roam--default-heading-title))
(org-noter-doc-property-in-notes t)
(org-noter-separate-notes-from-heading t)
(org-noter-show-notes-count-in-doc-mode-line nil)
(org-noter-hide-other t)
(org-noter-auto-save-last-location t)
(org-noter-insert-note-no-questions t)
(org-noter-default-notes-file-names nil)
(org-noter-notes-search-path (list config-org-roam-bibliography-notes-directory)))
:init
(defun config-org-roam--maybe-org-noter ()
(when (or (org-entry-get (point) "NOTER_DOCUMENT")
(org-entry-get (point) "NOTER_PAGE"))
(org-noter)))
(add-hook 'org-open-at-point-functions 'config-org-roam--maybe-org-noter)
:config
(defun config-org-roam--guess-pdf-title-for-notes ()
(let* ((most-recent-pdf (seq-find (lambda (it)
(with-current-buffer it
(derived-mode-p 'pdf-view-mode 'doc-view-mode)))
(buffer-list)))
(pdf-meta (pdf-info-metadata most-recent-pdf))
(found (alist-get 'title pdf-meta)))
(if (or (null found) (string-blank-p found))
"UNKNOWN TITLE"
found)))
(defun config-org-roam--insert-document-header-for-org-noter-notes ()
(org-with-wide-buffer
(goto-char (point-min))
(unless (string-match-p (rx "#+title:") (buffer-substring (line-beginning-position) (line-end-position)))
(let* ((key (file-name-base (buffer-file-name)))
(title (config-org-roam--guess-pdf-title-for-notes))
(inhibit-read-only t))
(insert (string-trim-left (format "
#+title: %s
#+roam_key: cite:%s
#+category: %s
#+roam_tags:
" title key key)))))))
(add-hook 'org-noter-root-headline-inserted-hook #'config-org-roam--insert-document-header-for-org-noter-notes))org-ref - insert and format bibtex references
(use-package org-ref
:custom
((org-ref-completion-library 'org-ref-helm-cite)
(org-ref-get-pdf-filename-function 'org-ref-get-pdf-filename-helm-bibtex)
(org-ref-note-title-format (string-trim-left "
* TODO %y - %t
:PROPERTIES:
:CUSTOM_ID: %k
:NOTER_DOCUMENT: %F
:ROAM_KEY: cite:%k
:AUTHOR: %9a
:JOURNAL: %j
:YEAR: %y
:VOLUME: %v
:PAGES: %p
:DOI: %D
:URL: %U
:END:
"))
(org-ref-notes-directory config-org-roam-bibliography-notes-directory)
(org-ref-notes-function 'orb-edit-notes)
;; Make detection regexps a little more robust.
(org-ref-url-date-re
(rx "<meta property=\"" (group (+? alnum) ":published") "\"" (+? (not (any ">"))) "content=\"" (group (+? nonl)) "\""))
(org-ref-url-author-re
(rx "<meta name=\"author\"" (+? (not (any ">"))) "content=\"" (group (+? nonl)) "\"")))
:config
(require 'org-ref-pdf)
(require 'org-ref-url-utils)
(defalias 'dnd-unescape-uri 'dnd--unescape-uri))org-roam-ui - Web browser interface to org-roam
(use-package org-roam-ui
:after org-roam
:custom
(org-roam-ui-sync-theme t)
(org-roam-ui-follow t)
(org-roam-ui-update-on-save t)
(org-roam-ui-open-on-start t))timekeep - Better timekeeping handling for org-clock
(use-package timekeep
:commands
timekeep-start
timekeep-stop
timekeep-find-client-buffer
timekeep-read-client-name
:general ("<f12>" (general-predicate-dispatch 'timekeep-start
(and (fboundp 'org-clocking-p) (org-clocking-p)) 'timekeep-stop))
:custom
(timekeep-cache-file (expand-file-name "timekeep" paths-cache-directory))
:config
(timekeep-mode +1)
(add-hook 'timekeep-agenda-should-update-hook #'org-funcs-agenda-dwim))orgtbl-aggregate - Add pivot tables to org-mode
(use-package orgtbl-aggregate
:after (org)
:demand t)ox-koma-letter - Export backend for letters via latex
(use-package ox-koma-letter
:demand t
:after ox)Add a link type for opening macOS GUI applications
(use-package org
:config
(require 'ol)
(defun org-link-complete-mac-app (&optional arg)
(cl-labels ((apps-in (dir)
(ignore-errors
(f-entries dir (lambda (path)
(and (equal (f-ext path) "app")
(not (string-prefix-p "." (f-filename path)))))))))
(let ((choices (thread-last (append (apps-in "/Applications")
(apps-in "/System/Applications")
(apps-in "/System/Applications/Utilities")
(apps-in "~/Applications"))
(seq-map #'f-base)
(seq-uniq)
(seq-sort #'string<)))
(choice (completing-read "Application:" choices)))
(concat "mac-app:" choice))))
(defun org-link-follow-mac-app (app-name &optional arg)
(start-process "open" " open" "open" "-a" app-name))
(org-link-set-parameters "mac-app"
:follow #'org-link-follow-mac-app
:complete #'org-link-complete-app))Load Lisp from other locations
(use-package org
:unless noninteractive
:config
(when (file-directory-p paths-org-lisp-directory)
(load (expand-file-name "init.el" paths-org-lisp-directory) t)))
(use-package ledger-mode
:unless noninteractive
:config
(when (file-directory-p paths-ledger-lisp-directory)
(load (expand-file-name "init.el" paths-ledger-lisp-directory) t)))File postamble
;; (provide 'config)
;;; config.el ends here