Skip to content

Latest commit

 

History

History

.emacs.d

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Emacs

Use lexical binding in this file

This org-mode file gets built into an elisp file, and we want this comment at the top for performance reasons.

;; -*- lexical-binding: t; -*-

Who Am I?

(setq user-full-name "Robin Schroer"
      user-login-name "sulami"
      user-mail-address "[email protected]"
      sulami/source-directory "~/src")

Better defaults

Discard the custom file

custom.el is hidden state, all config is declarative.

(setq custom-file (make-temp-file ""))

Hide Backups

This way we lose everything backups if the whole machine crashes, but we don’t accidentally leave backups around.

(setq backup-directory-alist '(("." . "~/emacs-backup/"))
      auto-save-file-name-transforms '((".*" "~/emacs-auto-save/" t))
      create-lockfiles nil)

Revert changed files

This reloads changed files which are open in Emacs but not edited.

(setq revert-without-query '(".*"))

No trash

(setq delete-by-moving-to-trash nil)

Remember recent files

(use-package recentf
  :straight nil
  :custom
  (recentf-max-saved-items 1024)
  :hook
  (after-init . recentf-mode))

Be quiet on startup

(setq inhibit-splash-screen t
      inhibit-startup-screen t
      inhibit-startup-message t)

Don’t tell me about keybindings

(setq suggest-key-bindings nil)

Make Tramp great again

These are just fixes to make TRAMP work and be reasonably fast. Mostly sourced from the internet, I don’t pretend to know how this actually works.

(setq tramp-default-method "ssh"
      tramp-ssh-controlmaster-options
      "-o ControlMaster=auto -o ControlPath='tramp.%%C'")

;; Various speedups
;; from https://www.gnu.org/software/emacs/manual/html_node/tramp/Frequently-Asked-Questions.html
(setq remote-file-name-inhibit-cache 3600
      tramp-completion-reread-directory-timeout nil
      vc-ignore-dir-regexp (format "%s\\|%s"
                                   vc-ignore-dir-regexp
                                   tramp-file-name-regexp)
      tramp-verbose 0)

;; Disable the history file on remote hosts
(setq tramp-histfile-override t)

;; Save backup files locally
;; from https://stackoverflow.com/a/47021266
(add-to-list 'backup-directory-alist
             (cons tramp-file-name-regexp "/tmp/emacs-backup/"))

Remember where we were

(require 'saveplace)
(save-place-mode 1)

Always follow symbolic links

(setq vc-follow-symlinks t)

Don’t ring the bell

(setq ring-bell-function 'ignore)

Don’t blink the cursor

I find this mostly distracting.

(blink-cursor-mode -1)

Start the scratch buffer empty

(setq initial-scratch-message "")

Enable winner-mode

This allows me to use winner-undo if I accidentally delete a window.

(add-hook 'after-init-hook 'winner-mode)

Default to Elisp

(setq initial-major-mode 'emacs-lisp-mode)

Switch to the help window

(setq help-window-select t)

Spaces > tabs

(setq-default indent-tabs-mode nil)

Tabs are 4 spaces

(setq-default tab-width 4)

Sentences end with a single space

(setq sentence-end-double-space nil)

Show trailing whitespace

It’s disabled by default, and then gets enabled for all file-based buffer modes, so not for REPLS and shells.

(setq-default show-trailing-whitespace nil)
(defun sulami/show-trailing-whitespace ()
  "Just sets `show-trailing-whitespace'."
  (setq show-trailing-whitespace t))
(add-hook 'prog-mode-hook 'sulami/show-trailing-whitespace)
(add-hook 'text-mode-hook 'sulami/show-trailing-whitespace)

Show empty lines

This shows vim-style tildes on the left fringe.

(when (display-graphic-p)
  (setq-default indicate-empty-lines t)
  (define-fringe-bitmap 'tilde [0 0 0 113 219 142 0 0] nil nil 'center)
  (setcdr (assq 'empty-line fringe-indicator-alist) 'tilde))

Enable so-long-mode

This disables expensive modes when a buffer has very long lines to prevent performance issues.

(if (version<= "27.1" emacs-version)
    (global-so-long-mode 1))

Highlight matching parentheses

I prefer this over using rainbow parentheses, which make it difficult to see what’s actually happening.

(show-paren-mode 1)

Scrolling

These settings were lifted off the internet™ and make scrolling with pointing devices feel more reasonable.

(setq mouse-wheel-progressive-speed nil
      mouse-wheel-scroll-amount '(1 ((shift) . 1) ((control) . nil)))

No line wrapping

At least as a default, much nicer when resizing windows.

(set-default 'truncate-lines t)
(setq line-move-visual nil)

UTF-8

(setq-default buffer-file-coding-system 'utf-8)
(setenv "LANG" "en_US.UTF-8")
(setenv "LC_ALL" "en_US.UTF-8")
(prefer-coding-system 'utf-8)

Spelling

Use aspell with British English.

(use-package flyspell
  :straight nil
  :custom
  (ispell-program-name "aspell")
  (ispell-extra-args (quote ("--sug-mode=ultra" "--lang=en_GB-ise")))
  (flyspell-sort-corrections nil)
  (flyspell-issue-message-flag nil)
  :hook
  (prog-mode . flyspell-prog-mode))

Enable erase buffer

(put 'erase-buffer 'disabled nil)

Y/N for yes or no questions

(fset 'yes-or-no-p 'y-or-n-p)

Ask before exiting

(setq confirm-kill-emacs 'yes-or-no-p)

Frame title

Set the frame title to the current project name. This is useful if I have several frames/Emacsen open and want to switch between them.

(setq frame-title-format
      (list :eval '(let ((p-name (projectile-project-name)))
		     (if (string-equal p-name "-")
			 "Emacs"
		       (concat "Emacs - " p-name)))))

Disable all the GUI

(if (and (fboundp 'tool-bar-mode)
         tool-bar-mode)
    (tool-bar-mode -1))
(if (fboundp 'menu-bar-mode) (menu-bar-mode -1))
(if (fboundp 'scroll-bar-mode) (scroll-bar-mode -1))
(if (fboundp 'tooltip-mode) (tooltip-mode -1))

Setup SSH via GPG-Agent

This assumes GPG-Agent is already running. Otherwise start it with gpg-agent --daemon.

(setenv "SSH_AUTH_SOCK" (string-trim (shell-command-to-string "gpgconf --list-dirs agent-ssh-socket")))

Enable recursive minibuffers

For example to yank while entering into the minibuffer.

(setq enable-recursive-minibuffers t)

macOS

Everything in here relates to macOS in some way.

Swap the modifier keys

The MacPorts build I’m using swaps the modifiers from what I’m used to, so I’m swapping them back.

(setq mac-command-modifier 'super
      mac-option-modifier 'meta)

Fix paste

Especially Alfred likes to paste with ⌘-v, so that needs to work.

(define-key global-map (kbd "s-v") 'yank)

Maximise with ⌘-Return

(define-key global-map (kbd "<s-return>") 'toggle-frame-maximized)

Mac font panel

(define-key global-map (kbd "s-f") #'mac-font-panel-mode)

Fix frame focus

The MacPorts Emacs version I’m using has the peculiar behaviour that requires menu-bar-mode to be enabled in order to focus the current frame when switching workspaces.

;; Use `mac-font-panel-mode' as a proxy to find out if this is the
;; MacPorts version.
(when (fboundp 'mac-font-panel-mode)
  (menu-bar-mode 1))

Get the macOS theme

(defun sulami/macos-dark-theme-p ()
  "Return non-nil if on macOS and currently in dark theme."
  (when (fboundp 'mac-application-state)
    (equal "NSAppearanceNameDarkAqua"
           (plist-get (mac-application-state) :appearance))))

Package management

use-package

Default straight to install anything use-package defines.

(setq straight-use-package-by-default t)

el-patch

Allows for patching functions in packages.

(use-package el-patch)

Dash

List library that comes in handy.

(use-package dash)

Updating all packages

(defun sulami/update-packages ()
  "Prunes and updates packages, revalidates patches."
  (straight-prune-build-directory)
  (straight-pull-all)
  (el-patch-validate-all)
  (straight-freeze-versions)
  (byte-recompile-directory "~/.emacs.d/straight/build" nil 'force))

Appearance

Font

Set the font to Fira Code and enable ligatures.

(let ((font "PragmataPro Mono 14"))
  (set-face-attribute 'default nil :font font)
  (set-frame-font font nil t))
;; (when (boundp 'mac-auto-operator-composition-mode)
;;   (mac-auto-operator-composition-mode))

Theme

I use doom-themes, mostly doom-solarized-light & doom-gruvbox.

There are some fixes to prevent themes from clashing, and I also disable most backgrounds as I find them distracting.

;; I like to live dangerously
(setq custom-safe-themes t)

(defconst sulami/light-theme 'doom-solarized-light)
(defconst sulami/dark-theme 'doom-gruvbox)

(defun sulami/disable-all-themes ()
  "Disables all custom themes."
  (interactive)
  (mapc #'disable-theme custom-enabled-themes))

(defun sulami/before-load-theme-advice (theme &optional no-confirm no-enable)
  "Disable all themes before loading a new one.

Prevents mixing of themes, where one theme doesn't override all faces
of another theme."
  (sulami/disable-all-themes))

(advice-add 'load-theme
            :before
            #'sulami/before-load-theme-advice)

(defun sulami/set-face-straight-underline (face)
  "Remove FACE's :underline style, if it's set"
  (when (display-graphic-p)
    (let ((old-attr (face-attribute face :underline)))
      (when (eq 'cons (type-of old-attr))
        (set-face-attribute face nil :underline (nth 3 old-attr))))))

(defun sulami/after-load-theme-advice (theme &optional no-confirm no-enable)
  "Unsets backgrounds for some org-mode faces.

Also changes squiggly underlines to straight ones."
  (require 'flycheck)
  (set-face-background 'outline-1 nil)
  (set-face-background 'org-block nil)
  (set-face-background 'org-block-begin-line nil)
  (set-face-background 'org-block-end-line nil)
  (set-face-background 'org-quote nil)
  (cl-loop for face in '(flyspell-incorrect
                         flyspell-duplicate
                         flycheck-error
                         flycheck-warning
                         flycheck-info)
           do (sulami/set-face-straight-underline face)))

(advice-add 'load-theme
            :after
            #'sulami/after-load-theme-advice)

(use-package doom-themes
  :after (dash)
  :init
  (setq doom-themes-enable-bold t
        doom-themes-enable-italic t)
  :config
  (doom-themes-org-config)
  ;; Set the default colourscheme according to the time of day
  :hook
  (after-init . (lambda ()
                  (when (display-graphic-p)
                    (let* ((hour-of-day (read (format-time-string "%H")))
                           (theme (if (or (not (sulami/macos-dark-theme-p))
                                          (<= 8 hour-of-day 17))
                                      sulami/light-theme
                                    sulami/dark-theme)))
                      (load-theme theme t)
                      (sulami/after-load-theme-advice theme))))))

(use-package mac-auto-theme
  :straight nil
  :no-require t
  :if (fboundp 'mac-application-state)
  :after (doom-themes)
  :hook
  (mac-effective-appearance-change . (lambda ()
                                       (when (display-graphic-p)
                                         (load-theme
                                          (if (sulami/macos-dark-theme-p)
                                              sulami/dark-theme
                                            sulami/light-theme)
                                          t)))))

Modeline

I use doom-modeline, without any icons, and patched to be regular height.

(use-package doom-modeline
  :hook (after-init . doom-modeline-mode)
  :custom
  (doom-modeline-icon nil)
  (doom-modeline-height 10)
  (doom-modeline-buffer-file-name-style 'relative-to-project)
  (doom-modeline-buffer-encoding nil)
  (doom-modeline-persp-name nil)
  (doom-modeline-vcs-max-length 36)
  :config/el-patch
  (defun doom-modeline--font-height ()
    "Calculate the actual char height of the mode-line."
    (let ((height (face-attribute 'mode-line :height)))
      ;; WORKAROUND: Fix tall issue of 27 on Linux
      ;; @see https://github.com/seagle0128/doom-modeline/issues/271
      (round
       (* (if (and (>= emacs-major-version 27)
                   (not (eq system-type 'darwin)))
              1.0
            (if doom-modeline-icon
                (el-patch-swap 1.68 1.0)
              (el-patch-swap 1.25 1.0)))
          (cond ((integerp height) (/ height 10))
                ((floatp height) (* height (frame-char-height)))
                (t (frame-char-height))))))))

Custom functions

Config

Open this file

(defun sulami/open-emacs-config ()
  "Opens the config file for our favourite OS."
  (interactive)
  (find-file sulami/emacs-config-file))

Reload this file

(defun sulami/reload-emacs-config ()
  "Loads the config file for our favourite OS."
  (interactive)
  (org-babel-load-file sulami/emacs-config-file))

Buffers

Rename buffer file

(defun sulami/rename-file-and-buffer ()
  "Rename the current buffer and file it is visiting."
  (interactive)
  (let ((filename (buffer-file-name)))
    (if (not (and filename (file-exists-p filename)))
        (message "Buffer is not visiting a file!")
      (let ((new-name (read-file-name "New name: " filename)))
        (cond
         ((vc-backend filename) (vc-rename-file filename new-name))
         (t
          (rename-file filename new-name t)
          (set-visited-file-name new-name t t)))))))

Switch to buffer shortcuts

(defun sulami/open-scratch-buffer ()
  "Opens the scratch buffer."
  (interactive)
  (switch-to-buffer "*scratch*"))

(defun sulami/open-message-buffer ()
  "Opens the message buffer."
  (interactive)
  (switch-to-buffer "*Messages*"))

(defun sulami/open-minibuffer ()
  "Focusses the minibuffer, if active."
  (interactive)
  (when (active-minibuffer-window)
    (select-window (minibuffer-window))))

Buffer line count

(defun sulami/buffer-line-count ()
  "Get the number of lines in the active buffer."
  (count-lines 1 (point-max)))

Delete buffer file

(defun sulami/delete-file-and-buffer ()
  "Deletes a buffer and the file it's visiting."
  (interactive)
  (when-let* ((file-name (buffer-file-name))
              (really (yes-or-no-p (format "Delete %s? "
                                           file-name))))
    (delete-file file-name)
    (kill-buffer)))

Copy buffer

(defun sulami/copy-buffer ()
  "Copies the entire buffer to the kill-ring."
  (interactive)
  (copy-region-as-kill 1 (point-max)))

Open the source directory

(defun sulami/open-source-dir ()
  (interactive)
  (find-file sulami/source-directory))

Toggle a terminal

(defun sulami/toggle-term ()
  "Opens global vterm, or switches to last buffer."
  (interactive)
  (let ((buf-name "*vterm*"))
    (cond
     ((eq major-mode 'vterm-mode)
      (evil-switch-to-windows-last-buffer))
     ((get-buffer buf-name)
      (switch-to-buffer buf-name))
     ((vterm buf-name)))))

Windows

Maximise a window

(defun sulami/toggle-maximise-window ()
  "Toggles maximising the current window.

From: https://gist.github.com/mads-hartmann/3402786"
  (interactive)
  (if (and (= 1 (length (window-list)))
           (assoc ?_ register-alist))
      (jump-to-register ?_)
    (progn
      (window-configuration-to-register ?_)
      (delete-other-windows))))

Run a shell command on a region

(defun sulami/shell-command-on-region (beg end cmd)
  (interactive "r\nsCommand: ")
  (shell-command-on-region beg end cmd t t))

Sort words

(defun sulami/sort-words (beg end)
  "Sorts words in region."
  (interactive "r")
  (sort-regexp-fields nil "\\w+" "\\&" beg end))

Kill matching lines

(defun sulami/kill-matching-lines (regexp &optional rstart rend interactive)
  "Kill lines containing matches for REGEXP.

See `flush-lines' or `keep-lines' for behavior of this command.

If the buffer is read-only, Emacs will beep and refrain from deleting
the line, but put the line in the kill ring anyway.  This means that
you can use this command to copy text from a read-only buffer.
\(If the variable `kill-read-only-ok' is non-nil, then this won't
even beep.)"
  (interactive
   (keep-lines-read-args "Kill lines containing match for regexp"))
  (let ((buffer-file-name nil)) ;; HACK for `clone-buffer'
    (with-current-buffer (clone-buffer nil nil)
      (let ((inhibit-read-only t))
        (keep-lines regexp rstart rend interactive)
        (kill-region (or rstart (line-beginning-position))
                     (or rend (point-max))))
      (kill-buffer)))
  (unless (and buffer-read-only kill-read-only-ok)
    ;; Delete lines or make the "Buffer is read-only" error.
    (flush-lines regexp rstart rend interactive)))

Toggle narrowing

(defun sulami/toggle-narrow ()
  "Toggles `narrow-to-defun' or `org-narrow-to-subtree'."
  (interactive)
  (if (buffer-narrowed-p)
      (widen)
    (if (eq major-mode 'org-mode)
        (org-narrow-to-subtree)
      (narrow-to-defun))))

Toggle line numbers

This one is faster than linum-mode.

(defun sulami/toggle-line-numbers ()
  "Toggles buffer line number display."
  (interactive)
  (setq display-line-numbers (not display-line-numbers)))

Find the font face used

This one is quite useful for debugging syntax highlighting. It’s adapted from here.

(defun sulami/what-face (pos)
  (interactive "d")
  (let ((face (or (get-char-property pos 'read-face-name)
                  (get-char-property pos 'face))))
    (if face
        (message "Face: %s" face)
      (message "No face at %d" pos))))

Create a random UUID

I need random UUIDs all the time. This generates one and places it in the clipboard, ready for pasting. Heavily dependent on macOS.

(defun sulami/random-uuid ()
  (interactive)
  (let ((uuid (s-trim (shell-command-to-string "uuidgen | tr '[:upper:]' '[:lower:]'"))))
    (kill-new uuid)
    (message "Generated UUID: %s" uuid)))

Copy the path & line at point

This is useful to run Rspec tests.

(defun sulami/rspec-path ()
  (interactive)
  (let ((fp (f-relative buffer-file-name (projectile-project-root)))
        (ln (1+ (line-number-at-pos))))
    (-> (format "%s:%d" fp ln)
        (message)
        (kill-new))))

Fill/unfill paragraph

Fills the current paragraph. If used again, “unfills” it, for pasting in places that doesn’t like hard line breaks, such as GitHub. “Taken from here.

(defun sulami/fill-or-unfill ()
  "Like `fill-paragraph', but unfill if used twice."
  (interactive)
  (let ((fill-column
         (if (eq last-command #'sulami/fill-or-unfill)
             (progn (setq this-command nil)
                    (point-max))
           fill-column)))
    (if (eq major-mode 'org-mode)
        (call-interactively #' org-fill-paragraph)
      (call-interactively #'fill-paragraph))))

General

General allows me to use fancy prefix keybindings.

I’m using a spacemacs-inspired system of a global leader key and a local leader key for major modes. Bindings are setup in the respective use-package declarations.

(use-package general
  :config
  (general-auto-unbind-keys)
  (general-evil-setup)
  (defconst leader-key "SPC")
  (general-create-definer leader-def
    :prefix leader-key
    :keymaps 'override
    :states '(normal visual))
  (defconst local-leader-key ",")
  (general-create-definer local-leader-def
    :prefix local-leader-key
    :keymaps 'override
    :states '(normal visual))
  (leader-def
    "" '(nil :wk "my lieutenant general prefix")
    ;; Prefixes
    "a" '(:ignore t :wk "app")
    "b" '(:ignore t :wk "buffer")
    "d" '(:ignore t :wk "dired")
    "f" '(:ignore t :wk "file")
    "f e" '(:ignore t :wk "emacs")
    "g" '(:ignore t :wk "git")
    "h" '(:ignore t :wk "help")
    "j" '(:ignore t :wk "jump")
    "k" '(:ignore t :wk "lisp")
    "m" '(:ignore t :wk "mail")
    "o" '(:ignore t :wk "org")
    "p" '(:ignore t :wk "project/perspective")
    "s" '(:ignore t :wk "search/spell")
    "t" '(:ignore t :wk "toggle")
    "w" '(:ignore t :wk "window")
    ;; General keybinds
    "\\" 'indent-region
    "|" 'sulami/shell-command-on-region
    "a c" 'calc
    "a s" 'shell
    "b e" 'erase-buffer
    "b d" 'kill-this-buffer
    "b D" 'kill-buffer-and-window
    "b m" 'sulami/open-message-buffer
    "b ." 'sulami/open-minibuffer
    "b r" 'sulami/rename-file-and-buffer
    "b s" 'sulami/open-scratch-buffer
    "b y" 'sulami/copy-buffer
    "d d" #'dired
    "d s" #'sulami/open-source-dir
    "f f" 'find-file
    "f e e" 'sulami/open-emacs-config
    "f e r" 'sulami/reload-emacs-config
    "f D" 'sulami/delete-file-and-buffer
    "f R" 'sulami/rename-file-and-buffer
    "h e" 'info-display-manual
    "h g" 'general-describe-keybindings
    "h l" 'view-lossage
    "h m" 'woman
    "h v" 'describe-variable
    "t a" 'auto-fill-mode
    "t l" 'toggle-truncate-lines
    "t r" 'refill-mode
    "t s" 'flyspell-mode
    "t n" 'sulami/toggle-line-numbers
    "t N" 'sulami/toggle-narrow
    "t w" 'whitespace-mode
    "w =" 'balance-windows
    "w f" 'make-frame
    "w m" 'sulami/toggle-maximise-window
    "w u" 'winner-undo)
  (general-define-key
   "s-m" #'suspend-frame
   "s-t" #'sulami/toggle-term
   "s-u" #'universal-argument
   "s-=" (lambda () (interactive) (text-scale-increase 0.5))
   "s--" (lambda () (interactive) (text-scale-decrease 0.5))
   "s-0" (lambda () (interactive) (text-scale-increase 0))
   "M-q" #'sulami/fill-or-unfill)
  (general-nmap "g r" #'xref-find-references)
  ;; Dired
  (general-define-key
   :keymaps 'dired-mode-map
   "<return>" 'dired-find-alternate-file))

Evil

This provides vim-style modal editing. There is quite a bit of boilerplate to make it work with the various components, but I really can’t stand the default Emacs keybindings.

(use-package evil
  :init
  (setq evil-want-C-u-scroll t
        evil-want-C-i-jump t
        evil-want-Y-yank-to-eol t
        evil-want-keybinding nil
        evil-ex-visual-char-range t
        evil-move-beyond-eol t
        evil-disable-insert-state-bindings t)
  :custom
  (evil-undo-system 'undo-fu)
  :config
  ;; This conflicts with the local leader
  (unbind-key "," evil-motion-state-map)

  (defun sulami/evil-shift-left-visual ()
    "`evil-shift-left`, but keeps the selection."
    (interactive)
    (call-interactively 'evil-shift-left)
    (evil-normal-state)
    (evil-visual-restore))

  (defun sulami/evil-shift-right-visual ()
    "`evil-shift-right`, but keeps the selection."
    (interactive)
    (call-interactively 'evil-shift-right)
    (evil-normal-state)
    (evil-visual-restore))

  :general
  (leader-def
   "TAB" #'evil-switch-to-windows-last-buffer
   "<tab>" #'evil-switch-to-windows-last-buffer
   "w d" #'evil-window-delete
   "w h" #'evil-window-move-far-left
   "w j" #'evil-window-move-very-bottom
   "w k" #'evil-window-move-very-top
   "w l" #'evil-window-move-far-right
   "w /" #'evil-window-vsplit
   "w -" #'evil-window-split)
  (general-imap
    "C-w" #'evil-delete-backward-word
    "C-k" #'evil-insert-digraph)
  (general-vmap
    ">" #'sulami/evil-shift-right-visual
    "<" #'sulami/evil-shift-left-visual)
  :hook (after-init . evil-mode))

evil-collection

This adds evil-keybindings for a lot of popular modes.

I have to disable some because they clash with my own.

(use-package evil-collection
  :after (evil)
  :config
  (setq evil-collection-mode-list
        (->> evil-collection-mode-list
             (delete 'company)
             (delete 'gnus)
             (delete 'lispy)))
  (evil-collection-init))

evil-org

Evil-keybindings for org/agenda.

(use-package evil-org
  :after (org)
  :config
  (require 'evil-org-agenda)
  :hook ((org-mode . evil-org-mode)
         (org-agenda-mode . evil-org-agenda-set-keys)))

evil-commentary

vim-commentary but for evil.

(use-package evil-commentary
  :hook (evil-mode . evil-commentary-mode))

evil-surround

vim-surround but for evil.

(use-package evil-surround
  :hook (evil-mode . global-evil-surround-mode))

evil-numbers

(use-package evil-numbers
  :defer t
  :general
  (general-nvmap
    "C-a" 'evil-numbers/inc-at-pt
    "C-z" 'evil-numbers/dec-at-pt))

Undo-fu

This just provides linear undo/redo. As an added bonus, it also does “undo in region”.

(use-package undo-fu
  :defer t
  :custom
  (undo-fu-allow-undo-in-region t)
  (undo-fu-ignore-keyboard-quit t))

Which key

This shows all available keybindings when I hit a key. Sometimes useful.

(use-package which-key
  :hook (after-init . which-key-mode))

Vertico

Vertico, the successor to Selectrum, is a great narrowing and selection tool, intended to replace Ivy & Helm.

(use-package vertico
  :custom
  (completion-in-region-function #'consult-completion-in-region)
  :general
  (leader-def
    "b b" #'consult-buffer
    "p b" #'consult-project-buffer)
  :hook
  (after-init . vertico-mode))

Marginalia

(use-package marginalia
  :hook
  (vertico-mode . marginalia-mode))

Consult

Consult is counsel for vertico, in that it adds vertico support for various commands that do not use read-string normally.

consult-flycheck cannot be autoloaded via consult, so it needs to be loaded separately.

(use-package consult
  :defer t
  :straight
  '(consult
    :type git
    :host github
    :repo "minad/consult"
    :branch "main")
  :custom
  (consult-preview-key nil)
  (xref-show-xrefs-function #'consult-xref)
  (xref-show-definitions-function #'consult-xref)
  :config
  ;; Eshell only defines its locally keymap when you launch it, so we
  ;; have to add bindings with a hook.
  (defun sulami/consult-setup-eshell-bindings ()
    (general-imap
      :keymaps 'eshell-mode-map
      "C-r" 'consult-history))
  :general
  (leader-def
    "f r" 'consult-recent-file
    "j i" 'consult-imenu
    "j I" 'consult-project-imenu
    "j o" 'consult-outline
    "s s" 'consult-line)
  (general-imap
    "M-y" 'consult-yank-pop)
  (general-nmap
    "M-y" 'consult-yank-pop)
  (general-imap
    :keymaps 'shell-mode-map
    "C-r" 'consult-history)
  :hook
  ((eshell-mode . sulami/consult-setup-eshell-bindings)))

(use-package consult-flycheck
  :general
  (leader-def
    "j e" 'consult-flycheck))

Orderless

Orderless provides a minibuffer completion style that is “whitespace separated words in any order.” Very useful if you don’t know exactly what you’re looking for.

(use-package orderless
  :defer t
  :custom
  (completion-styles '(orderless basic)))

Flyspell-correct

(use-package flyspell-correct
  :defer t
  :general
  (leader-def
    "s c" 'flyspell-correct-at-point))

Corfu

Corfu does completion via a dropdown that automatically pops up while typing. I can select a match if I want to, but ignore the dropdown if I don’t.

(use-package corfu
  :custom
  (corfu-auto t)
  (corfu-quit-no-match 'separator)
  :hook (after-init . global-corfu-mode))

Yasnippet

Snippets. I have a few custom ones. yasnippet-snippets is a huge bundle of useful snippets for all kinds of modes.

(use-package yasnippet
  :general
  (general-imap
    "C-y" 'yas-insert-snippet)
  (:keymaps 'yas-minor-mode-map
   "<tab>" nil
   "TAB" nil
   "<ret>" nil
   "RET" nil)
  :config
  (yas-reload-all)
  :hook
  ((text-mode . yas-minor-mode)
   (prog-mode . yas-minor-mode)))

(use-package yasnippet-snippets
  :defer t
  :after (yasnippet))

Popper

Popper deals with all those temporary popup windows.

(use-package popper
  :after (projectile)
  :init
  (setq popper-reference-buffers
        '("\\*Messages\\*"
          "\\*Async Shell Command\\*"
          help-mode
          helpful-mode
          org-roam-mode
          backtrace-mode
          compilation-mode
          vterm-mode
          rustic-compilation-mode
          rustic-cargo-run-mode
          rustic-cargo-test-mode
          rustic-rustfmt-mode
          rspec-compilation-mode))
  (setq popper-display-control t
        popper-display-function #'display-buffer-use-least-recent-window)
  (popper-mode +1)
  (popper-echo-mode +1)
  :custom
  (popper-group-function #'popper-group-by-projectile)
  :general
  ("s-p" #'popper-toggle-latest
   "s-g" #'popper-cycle))

Parentheses

Keeps my parentheses balanced.

Lispyville

I use LispyVille for all Lisp major modes, as it does some additional magic around spacing, comments, and more.

N.B. This currently pulls in a lot of dependencies for no good reason, including swiper & hydra.

(use-package lispyville
  :defer t
  :custom
  (lispy-close-quotes-at-end-p t)
  :config
  (lispyville-set-key-theme '(operators
                              c-w
                              additional-motions
                              commentary
                              slurp/barf-lispy
                              additional-wrap))
  :general
  (general-imap
    :keymaps 'lispyville-mode-map
    "(" 'lispy-parens
    "[" 'lispy-brackets
    "{" 'lispy-braces
    "\"" 'lispy-quotes
    ")" 'lispy-right-nostring
    "]" 'lispy-right-nostring
    "}" 'lispy-right-nostring
    "DEL" 'lispy-delete-backward-or-splice-or-slurp)
  :hook
  ((emacs-lisp-mode . lispyville-mode)
   (lisp-mode . lispyville-mode)
   (scheme-mode . lispyville-mode)
   (clojure-mode . lispyville-mode)
   (cider-repl-mode . lispyville-mode)
   (monroe-mode . lispyville-mode)
   (racket-mode . lispyville-mode)))

Electric Pair Mode

All other modes just use electric-pair-mode, which is built into Emacs already, for automatically matching parentheses. The main reason for this divide being the whitespace changes done by LispyVille interfering with non-lisp syntax.

(use-package electric-pair-mode
  :straight nil
  :hook
  ((text-mode . electric-pair-local-mode)
   (prog-mode . electric-pair-local-mode)))

Code folding

(use-package code-folding
  :straight nil
  :config
  (defun sulami/close-defun-fold ()
    "Hide the top-level definition at point."
    (interactive)
    (beginning-of-defun)
    (hs-hide-block))
  :general
  (general-nmap
    "z C" #'sulami/close-defun-fold)
  :hook
  (prog-mode . hs-minor-mode))

Exec Path From Shell

(use-package exec-path-from-shell
  :init
  (exec-path-from-shell-initialize))

Dumb jump

Uses rg to jump to definition. Zero setup. Not always correct, but usually good enough. Much less of a hassle than LSP.

(use-package dumb-jump
  :after (evil)
  :custom
  (dumb-jump-prefer-searcher 'rg)
  (dumb-jump-selector 'completing-read)
  :config
  (add-hook 'xref-backend-functions #'dumb-jump-xref-activate))

Deadgrep

(use-package deadgrep
  :defer t)

Wgrep

This allows running rgrep and then writing to the result buffer, modifying the files matched in place. Quite useful for sweeping changes.

(use-package wgrep
  :defer t
  :commands (wgrep-change-to-wgrep-mode)
  :config
  (setq wgrep-auto-save-buffer t)
  :general
  (local-leader-def
    :keymaps 'grep-mode-map
    "w" 'wgrep-change-to-wgrep-mode))

Highlight TODO

Highlights certain keywords in comments, like TODO and FIXME.

(use-package hl-todo
  :defer t
  :config
  (add-to-list 'hl-todo-keyword-faces
               `("N\.?B\.?" . ,(cdr (assoc "NOTE" hl-todo-keyword-faces))))
  :hook (after-init . global-hl-todo-mode))

Highlight symbol

I only enable this every now and then.

(use-package auto-highlight-symbol
  :custom
  (ahs-idle-interval 0.1)
  :general
  (leader-def "t h" 'auto-highlight-symbol-mode))

Projectile

Manages projects (usually git repositories, but flexible).

(use-package projectile
  :custom
  (projectile-switch-project-action 'projectile-find-file)
  (projectile-completion-system 'default)
  :config
  (defun sulami/projectile-rg ()
    (interactive)
    (let ((vertico-count 30))
      (consult-ripgrep (projectile-project-root))))

  (defun sulami/projectile-rg-thing-at-point ()
    (interactive)
    (let ((vertico-count 30))
      (consult-ripgrep (projectile-project-root)
                       (thing-at-point 'symbol t))))

  (defun sulami/toggle-project-root-eshell ()
    "Opens eshell, if possible in the project root."
    (interactive)
    (cond
     ((eq major-mode 'eshell-mode)
      (evil-switch-to-windows-last-buffer))
     ((projectile-project-p (f-dirname (or (buffer-file-name)
                                           "")))
      (call-interactively #'projectile-run-eshell))
     ((eshell))))

  (defun sulami/toggle-project-root-shell ()
    "Opens shell, if possible in the project root."
    (interactive)
    (cond
     ((eq major-mode 'shell-mode)
      (evil-switch-to-windows-last-buffer))
     ((projectile-project-p (f-dirname (or (buffer-file-name)
                                           "")))
      (call-interactively #'projectile-run-shell))
     ((shell))))

  (defun sulami/toggle-project-root-term ()
    "Opens vterm, if possible in the project root."
    (interactive)
    (cond
     ((eq major-mode 'vterm-mode)
      (evil-switch-to-windows-last-buffer))
     ((projectile-project-p (f-dirname (or (buffer-file-name)
                                           "")))
      (let ((default-directory (projectile-project-root))
            (buf-name (concat "*vterm " (projectile-project-name) "*")))
        (if (get-buffer buf-name)
            (switch-to-buffer buf-name)
          (vterm buf-name))))
     ((vterm))))

  ;; Don't do projectile stuff on remote files
  ;; from https://github.com/syl20bnr/spacemacs/issues/11381#issuecomment-481239700
  (defadvice projectile-project-root (around ignore-remote first activate)
    (unless (file-remote-p default-directory) ad-do-it))

  :general
  (leader-def
    "d p" #'projectile-dired
    "p c" #'projectile-compile-project
    "p p" #'projectile-switch-project
    "p f" #'projectile-find-file
    "p k" #'projectile-kill-buffers
    "p s" #'sulami/toggle-project-root-shell
    "p t" #'sulami/toggle-project-root-term
    "s p" #'sulami/projectile-rg
    "s P" #'sulami/projectile-rg-thing-at-point)
  ("s-'" #'sulami/toggle-project-root-eshell)
  :hook (after-init . projectile-global-mode))

Winum

Number windows and allow me to switch to them.

(use-package winum
  :general
  ("s-1" 'winum-select-window-1
   "s-2" 'winum-select-window-2
   "s-3" 'winum-select-window-3
   "s-4" 'winum-select-window-4
   "s-5" 'winum-select-window-5
   "s-6" 'winum-select-window-6
   "s-7" 'winum-select-window-7
   "s-8" 'winum-select-window-8
   "s-9" 'winum-select-window-9)
  (leader-def
    "w 1" 'winum-select-window-1
    "w 2" 'winum-select-window-2
    "w 3" 'winum-select-window-3
    "w 4" 'winum-select-window-4
    "w 5" 'winum-select-window-5
    "w 6" 'winum-select-window-6
    "w 7" 'winum-select-window-7
    "w 8" 'winum-select-window-8
    "w 9" 'winum-select-window-9)
  :hook (after-init . winum-mode))

Org mode

(use-package org
  :custom
  (calendar-date-style 'iso "YYYY-MM-DD")
  (org-directory "~/Documents/Notes")
  (calendar-week-start-day 1 "Weeks start on Monday")
  (org-complete-tags-always-offer-all-agenda-tags t "Autocomplete tags")
  (org-edit-src-content-indentation 0 "Do not indent src blocks")
  (org-export-initial-scope 'subtree "Default export to the current subtree")
  (org-export-with-toc nil "Omit the TOC when exporting")
  (org-export-with-author nil "Omit the author when exporting")
  (org-footnote-auto-adjust t "Automatically renumber footnotes")
  (org-footnote-section nil "Footnotes go into the section they are referenced in")
  (org-hide-emphasis-markers t "Hide emphasis markers")
  (org-log-into-drawer t "Log workflow changes into a drawer")
  (org-src-preserve-indentation nil "Remove leading whitespace when editing src blocks")
  (org-src-window-setup 'current-window "Show src editing in the current window")
  (org-use-fast-todo-selection t "Use fast selection for workflow states")
  (org-preview-latex-image-directory "~/.cache/org-latex/" "Store latex previews in cache")
  (org-agenda-files `(,(concat org-roam-directory "/" org-roam-dailies-directory)))
  :config
  ;; Open file links in same window.
  (setf (cdr (assoc 'file org-link-frame-setup)) 'find-file)
  (defun sulami/org-return ()
    "`org-return' with better plain list handling.

If inside a plain list, insert a new list item. If the current list
item is empty, remove it instead. Essentially imitating Google Docs."
    (interactive)
    (if (org-at-item-p)
        (let* ((begin (org-in-item-p))
               (struct (org-list-struct))
               (end (org-list-get-item-end begin struct))
               (indent (org-list-get-ind begin struct))
               (bullet (org-list-get-bullet begin struct))
               (checkbox (org-list-get-checkbox begin struct))
               (type (org-list-get-list-type begin
                                             struct
                                             (org-list-prevs-alist struct)))
               ;; Different factors alter the amount of whitespace.
               (whitespace (+ 1
                              (if (= end (point-max)) -1 0)
                              (if (= end (point-max) (1+ (point)))
                                  1 0)
                              (if checkbox 1 0)
                              (if (equal 'descriptive type) 3 0)
                              (if (and checkbox
                                       (equal 'descriptive type))
                                  1 0))))
          (if (zerop (- end
                        begin
                        indent
                        (length bullet)
                        (length checkbox)
                        whitespace))
              (progn (org-list-delete-item begin struct)
                     (insert "\n")
                     (backward-char))
            (evil-org-open-below 0)))
      (org-return)))
  :config/el-patch
  (defun org-link-escape (link)
    "Backslash-escape sensitive characters in string LINK."
    (el-patch-wrap 3 0
      (replace-regexp-in-string
       " "
       "%20"
       (replace-regexp-in-string
        (rx (seq (group (zero-or-more "\\")) (group (or string-end (any "[]")))))
        (lambda (m)
          (concat (match-string 1 m)
	          (match-string 1 m)
	          (and (/= (match-beginning 2) (match-end 2)) "\\")))
        link nil t 1))))
  :general
  (local-leader-def
    :keymaps 'org-mode-map
    :states '(normal)
    "a" 'org-archive-subtree
    "d" 'org-deadline
    "e" '(org-export-dispatch :wk "org-export-dispatch")
    "f" 'org-fill-paragraph
    "l" 'org-insert-link
    "L" 'org-store-link
    "R" 'sulami/org-refile-in-current-file
    "s" 'org-schedule
    "S" 'org-babel-switch-to-session
    "T" 'org-babel-tangle
    "w" 'org-todo
    "W" '((lambda ()
            (interactive)
            (org-todo '(4)))
          :wk "org-todo (with note)"))
  ;; Fix plain lists.
  (general-imap
    :keymaps 'org-mode-map
    "RET" 'sulami/org-return)
  :hook
  ((org-mode . auto-fill-mode)
   (org-mode . flyspell-mode)
   (org-mode . org-indent-mode)))

Add more workflow states

(setq org-todo-keywords '((sequence "TODO(t)"
                                    "WIP(p)"
                                    "WAITING(w)"
                                    "|"
                                    "DONE(d)"
                                    "CANCELLED(c)")))

Save when I change a workflow state

(add-hook 'org-trigger-hook 'save-buffer)

Enable babel for more languages

(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t)
   (shell . t)
   (python . t)))

Use drawers for source block evaluation

(add-to-list 'org-babel-default-header-args '(:results . "replace drawer"))

Disable ligatures in org-mode

(add-hook 'org-mode-hook
          (lambda ()
            (auto-composition-mode -1)))

Archiving

  • archive into one shared file
  • auto-save
(setq org-archive-location "~/Documents/Notes/archive.org::"
      org-archive-subtree-add-inherited-tags t)

Capture

My capture templates. Also reload buffers before attempting to capture to avoid overwriting iCloud changes.

(setq org-capture-templates
      '(("b" "Blog idea" entry
         (file "blog.org")
         "* %^{title}\n%u\n%?"
         :prepend t)
        ("f" "File link" entry
         (file "inbox.org")
         "* %^{title}\n%a\n%?")
        ("n" "Note" entry
         (file "inbox.org")
         "* %^{title}\n%u\n%?")
        ("t" "Thought" entry
         (file "thoughts.org")
         "* %^{title}\n%u\n%?")))

org-gfm

This gives me org-mode->github flavoured markdown export.

(use-package ox-gfm
  :defer 3
  :after org)

org-present

Useful to do simple screen share presentations.

(use-package org-present
  :hook
  ((org-present-mode . (lambda ()
                         (org-present-big)
                         (org-display-inline-images)
                         (org-present-hide-cursor)
                         (org-present-read-only)))
   (org-present-mode-quit . (lambda ()
                              (org-present-small)
                              (org-remove-inline-images)
                              (org-present-show-cursor)
                              (org-present-read-write)))))

org-roam

(use-package org-roam
  :init
  (setq org-roam-directory "~/Documents/Notes/org-roam")
  (setq org-roam-mode-sections (list #'org-roam-backlinks-section
                                     #'org-roam-reflinks-section
                                     ;; #'org-roam-unlinked-references-section
                                     ))
  (setq org-roam-completion-everywhere t)
  (setq org-roam-capture-templates
        '(("d" "default" plain "%?"
           :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
                              "#+title: ${title}\n")
           :unnarrowed t)
          ("b" "book" plain "%?"
           :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
                              ":PROPERTIES:\n:Author: \n:Published: \n:Consumed: \n:END:\n#+title: ${title}\n#+filetags: :book:\n\n")
           :unnarrowed t)))
  :general
  (leader-def
    "o" '(:ignore t :which-key "org")
    "o c" #'org-roam-capture
    "o f" #'org-roam-node-find
    "o t" #'org-roam-dailies-goto-today
    "o T" #'org-roam-dailies-goto-tomorrow
    "o y" #'org-roam-dailies-goto-yesterday
    "o d" #'org-roam-dailies-goto-date
    "o g" #'org-roam-graph
    "o S" #'org-roam-db-sync)
  (local-leader-def
    :keymaps 'org-mode-map
    "r" '(:ignore t :which-key "roam")
    "r i" #'org-roam-node-insert
    "r I" #'org-id-get-create
    "r t" #'org-roam-tag-add
    "r T" #'org-roam-tag-remove
    "r a" #'org-roam-alias-add
    "r A" #'org-roam-alias-remove
    "r r" #'org-roam-ref-add
    "r R" #'org-roam-ref-remove
    "r b" #'org-roam-buffer-toggle
    "r n" #'org-roam-dailies-goto-next-note
    "r p" #'org-roam-dailies-goto-previous-note)
  :hook (after-init . org-roam-db-autosync-mode))

(use-package org-roam-ui)

Magit

This is probably the single best interface for Git out there.

(use-package magit
  :custom
  (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
  :config
  (defun sulami/clone-repo (url)
    "Clone the repo at URL into `sulami/source-directory'"
    (interactive "sURL: ")
    (let* ((repo-name (magit-clone--url-to-name url))
           (target-dir (concat sulami/source-directory "/" repo-name)))
      (magit-clone-regular url target-dir nil)))
  (defun sulami/reset-repo ()
    "Reset the current repo to its default state"
    (interactive)
    (magit-checkout (magit-main-branch))
    (magit-pull-from-upstream nil))
  :general
  (leader-def
    "g b" #'magit-blame-addition
    "g h" #'magit-log
    "g n" #'sulami/reset-repo
    "g s" #'magit-status)
  :hook
  ((shell-mode . with-editor-export-editor)
   (term-mode . with-editor-export-editor)
   (eshell-mode . with-editor-export-editor)
   (git-commit-setup . git-commit-turn-on-flyspell)))

Git link

This package gets me the link to a git repository or line in a file.

(use-package git-link
  :init
  (defun open-git-link-in-browser ()
    (interactive)
    (let ((git-link-open-in-browser t))
      (git-link "origin" (line-number-at-pos) (line-number-at-pos))))
  (defun open-git-repo-in-browser ()
    (interactive)
    (let ((git-link-open-in-browser t))
      (git-link-homepage "origin")))
  :general
  (leader-def
   "g l" 'git-link
   "g L" 'open-git-link-in-browser
   "g r" 'git-link-homepage
   "g R" 'open-git-repo-in-browser))

Changelog

This is a convenience function to generate a changelog for a PR.

(defun sulami/magit-changelog ()
  "Generate a git changelog and copy it to the kill ring"
  (interactive)
  (kill-new
   (format "Best reviewed commit-by-commit:\n\n%s"
           (shell-command-to-string
            (format "git log %s..HEAD --reverse --pretty=format:'%s'"
                    (magit-main-branch)
                    "### %h %s%n%n%b")))))

Flycheck

Flycheck does automatic linting. I enable it mostly manually, as I don’t have a lot of linters setup.

(use-package flycheck
  :after (nix-sandbox)
  :config
  ;; Disable flycheck on-the-fly-checking if the line count exceeds 2000.
  (setq flycheck-check-syntax-automatically
        (if (> (sulami/buffer-line-count) 2000)
            (delete 'idle-change flycheck-check-syntax-automatically)
          (add-to-list 'flycheck-check-syntax-automatically 'idle-change))
        ;; Resolve checker commands in Nix environments.
        flycheck-command-wrapper-function
        (lambda (command) (apply 'nix-shell-command (nix-current-sandbox) command))
        flycheck-executable-find
        (lambda (cmd) (nix-executable-find (nix-current-sandbox) cmd)))
  :custom
  (flycheck-emacs-lisp-load-path 'inherit)
  :general
  (leader-def "t c" 'flycheck-mode)
  :hook
  ((clojure-mode . flycheck-mode)
   (rust-mode . flycheck-mode)
   (go-mode . flycheck-mode)))

Emacs Lisp

Just some bindings for interacting with Emacs Lisp.

(local-leader-def
  :keymaps 'emacs-lisp-mode-map
  "e" '(:ignore t :wk "eval")
  "e b" 'eval-buffer
  "e e" 'eval-last-sexp
  "e f" 'eval-defun
  "e r" 'eval-region)

Eshell

Eshell is my main shell these days, mostly because it integrates so well with Emacs. On rare occasions I use a terminal emulator (usually also inside Emacs) with zsh.

Prompt

(setq eshell-prompt-function
      (lambda ()
        (concat
         (when (not (eshell-exit-success-p))
           (concat
            "<"
            (propertize (number-to-string eshell-last-command-status)
                        'face `(:foreground "red"))
            "> "))
         (abbreviate-file-name (eshell/pwd))
         " λ "))
      eshell-prompt-regexp
      (rx (opt "<" (1+ digit) "> ")
          (1+ anything)
          " λ "))

Aliases

This just loads my aliases. They are auto-generated from my zsh aliases, so that the two are always in sync.

(setq eshell-aliases-file "~/.emacs.d/aliases")

Completion

Eshell doesn’t do context-aware autocompletion by default and defaults to completing filenames instead. Luckily we can easily define custom completion handlers for commands.

Disable the completion buffer

This swaps the terrible popup buffer that eshell opens when I hit TAB for a read-string completion.

The binding has to happen here in a hook because eshell-mode-map isn’t available before eshell is started.

(add-hook
 'eshell-mode-hook
 (lambda ()
   (setq completion-at-point-functions '(comint-completion-at-point t))
   (define-key eshell-mode-map (kbd "TAB") 'completion-at-point)
   (define-key eshell-mode-map (kbd "<tab>") 'completion-at-point)))

Sudo

(defun pcomplete/sudo ()
  "Completion rules for the `sudo' command."
  (let ((pcomplete-ignore-case t))
    (pcomplete-here (funcall pcomplete-command-completion-function))
    (while (pcomplete-here (pcomplete-entries)))))

Timestamps

I like having timestamps in my shell history, so that I can use the history for logs later on.

(defun sulami/eshell-insert-timestamp ()
  (when eshell-mode
    (save-excursion
      (goto-char eshell-last-input-start)
      (let ((ts (format-time-string "[%FT%TZ] " nil t)))
        (insert ts)
        (put-text-property eshell-last-input-start
                           (+ eshell-last-input-start
                              (length ts))
                           'face
                           'eshell-prompt)))))

(add-hook 'eshell-pre-command-hook #'sulami/eshell-insert-timestamp)

Ediff

Just diffing. I don’t have any strong opinions on it, ediff does the job. I’m sure there are cool features I’m not aware of.

Ignore whitespace changes

(setq ediff-diff-options "-w")

Don’t create a new frame for the control window

(setq ediff-window-setup-function 'ediff-setup-windows-plain)

Split horizontally by default

(setq ediff-split-window-function 'split-window-horizontally)

Pick both sides

Stolen from Stig.

(defun sulami/ediff-copy-both-to-C ()
  (interactive)
  (ediff-copy-diff ediff-current-difference nil 'C nil
                   (concat
                    (ediff-get-region-contents ediff-current-difference 'A ediff-control-buffer)
                    (ediff-get-region-contents
                     ediff-current-difference 'B
                     ediff-control-buffer))))

(defun sulami/add-d-to-ediff-mode-map ()
  (define-key ediff-mode-map "d" #'sulami/ediff-copy-both-to-C))

(add-hook 'ediff-keymap-setup-hook #'sulami/add-d-to-ediff-mode-map)

Dired

Dired+

This brings some nice quality of life extensions to dired.

(use-package dired+
  :custom
  (diredp-hide-details-initially-flag nil))

Enable find-alternate-file

This causes RET to open the target in the current buffer, instead of a new buffer. As a result, when traversing directories I don’t get one buffer per step, which quickly gets annoying.

At some point I need to rebind ^ as well.

(put 'dired-find-alternate-file 'disabled nil)

Always show me current data

(add-hook 'dired-mode-hook 'auto-revert-mode)

Use human-readable formats

(setq-default dired-listing-switches "-alh")

Copy recursively by default

(setq dired-recursive-copies 'always)

Woman

man and woman are manual page readers for emacs. man just calls the system man and shows the results in a buffer, while woman is a full man page parser in Emacs Lisp. woman is much faster, but does not support all formats of man pages. To solve this issue, I’m patching woman to fall back to man if it fails to render, which is still fast because it doesn’t require man to build a man page index, as we just pass it the correct file already.

(use-package man
  :straight nil
  :after (woman)
  :defer t
  :commands (woman)
  :config/el-patch
  (defun woman (&optional topic re-cache)
  "Browse UN*X man page for TOPIC (Without using external Man program).
The major browsing mode used is essentially the standard Man mode.
Choose the filename for the man page using completion, based on the
topic selected from the directories specified in `woman-manpath' and
`woman-path'.  The directory expansions and topics are cached for
speed.  With a prefix argument, force the caches to be
updated (e.g. to re-interpret the current directory).

Used non-interactively, arguments are optional: if given then TOPIC
should be a topic string and non-nil RE-CACHE forces re-caching."
    (interactive (list nil current-prefix-arg))
    ;; The following test is for non-interactive calls via gnudoit etc.
    (if (or (not (stringp topic)) (string-match-p "\\S " topic))
        (let ((file-name (woman-file-name topic re-cache)))
          (if file-name
              (el-patch-swap
                (woman-find-file file-name)
                (condition-case nil
                    (woman-find-file file-name)
                  (error (progn
                           (message "WoMan failed to format %s, falling back to `man'..." file-name)
                           (kill-buffer (alist-get file-name woman-buffer-alist))
                           (pop-to-buffer (man file-name))))))
            (message
             "WoMan Error: No matching manual files found in search path")
            (ding)))
      (message "WoMan Error: No topic specified in non-interactive call")
      (ding))))

Helpful

Helpful provides better *Help* buffers, with niceties such as “where is this symbol referenced”, and the full source code.

(use-package helpful
  :commands (helpful-symbol helpful-key)
  :config/el-patch
  (defun helpful-symbol (symbol)
    "Show help for SYMBOL, a variable, function or macro.

See also `helpful-callable' and `helpful-variable'."
    (interactive
     (list (helpful--read-symbol
            "Symbol: "
            (el-patch-wrap 2 1
              (condition-case nil
                  (helpful--symbol-at-point)
                (error nil)))
            #'helpful--bound-p)))
    (let ((c-var-sym (helpful--convert-c-name symbol t))
          (c-fn-sym (helpful--convert-c-name symbol nil)))
      (cond
       ((and (boundp symbol) (fboundp symbol))
        (if (y-or-n-p
             (format "%s is a both a variable and a callable, show variable?"
                     symbol))
            (helpful-variable symbol)
          (helpful-callable symbol)))
       ((fboundp symbol)
        (helpful-callable symbol))
       ((boundp symbol)
        (helpful-variable symbol))
       ((and c-fn-sym (fboundp c-fn-sym))
        (helpful-callable c-fn-sym))
       ((and c-var-sym (boundp c-var-sym))
        (helpful-variable c-var-sym))
       (t
        (user-error "Not bound: %S" symbol)))))

  :general
  (leader-def
    "h d" 'helpful-symbol
    "h f" 'helpful-function
    "h k" 'helpful-key))

Esup

This allows me to benchmark Emacs startup.

(use-package esup
  :defer t
  :commands (esup))

ERC

IRC. I don’t use IRC a lot, but every now and then. As such I like ERC because it works reasonably well out of the box, and I don’t need to yak shave an awful lot.

(use-package erc
  :straight nil
  :defer t
  :commands (erc sulami/erc)
  :custom
  (erc-nick "sulami")
  (erc-join-buffer 'bury)
  (erc-hide-list '("JOIN" "PART" "QUIT"))
  (erc-lurker-hide-list '("JOIN" "PART" "QUIT"))
  (erc-rename-buffers t)
  (erc-interpret-mirc-color t)
  (erc-timestamp-only-if-changed-flag nil)
  (erc-timestamp-format "%H:%M ")
  (erc-fill-prefix nil)
  (erc-fill-function 'erc-fill-variable)
  (erc-insert-timestamp-function 'erc-insert-timestamp-left)
  (erc-autojoin-channels-alist '(("irc.libera.chat" "#nixers_net")
                                 ("irc.circleci.com" "#general")))
  :config
  (add-to-list 'erc-modules 'keep-place)
  (add-to-list 'erc-modules 'spelling)
  (defun sulami/erc ()
    (interactive)
    (erc :server "irc.libera.chat"
         :port 6667
         :nick "sulami"
         :password (string-trim (shell-command-to-string "pass libera/password")))
    (erc :server "irc.circleci.com"
         :port 6667
         :nick "robins"
         :password (string-trim (shell-command-to-string "pass circleci/irc/password"))))
  :general
  (leader-def
    "a i" 'erc-track-switch-buffer))

Email

This is my email setup. I fetch email into a local maildir, which is available offline, and also much faster.

(setq message-directory "~/.mail"
      message-kill-buffer-on-exit t
      message-send-mail-function 'message-send-mail-with-sendmail
      message-sendmail-envelope-from 'header
      sendmail-program "msmtp"
      mail-specify-envelope-from t
      mail-envelope-from 'header)

Notmuch

notmuch is a performant email tagging system, which I use to sort and view emails in my local maildir.

(use-package notmuch
  :defer t
  :custom
  (notmuch-search-oldest-first nil)
  (notmuch-always-prompt-for-sender t)
  (notmuch-mua-cite-function 'message-cite-original-without-signature)
  (notmuch-fcc-dirs '((".*@sulami.xyz" . "fastmail/Sent +sent -inbox -unread")
                      (".*@peerwire.org" . "fastmail/Sent +sent -inbox -unread")
                      ("[email protected]" . "\"circleci-gmail/[Gmail]/Sent Mail\" +sent -inbox -unread")))
  :config
  (defun notmuch-inbox ()
    (interactive)
    (notmuch-search "tag:inbox" t))
  (defun notmuch-unread ()
    (interactive)
    (notmuch-search "tag:unread" t))
  (defun sulami/notmuch-search-open-pr ()
    "Open the pull request referenced in an email."
    (interactive)
    (when (and (eq major-mode 'notmuch-search-mode)
               (-contains? (notmuch-search-get-tags)
                           "github"))
      (let ((subject (substring-no-properties (notmuch-search-find-subject)))
            (regexp (rx string-start
                        "[" (group-n 1 (+? anychar)) "]"
                        (* anychar)
                        "(PR #" (group-n 2 (+ digit)) ")"
                        string-end)))
        (string-match regexp subject)
        (let ((project-slug (match-string 1 subject))
              (pr-number (match-string 2 subject)))
          (browse-url (format "https://github.com/%s/pull/%s" project-slug pr-number))))))
  :general
  (leader-def
    "m i" 'notmuch-inbox
    "m m" 'notmuch
    "m n" 'notmuch-mua-new-mail
    "m r" 'notmuch-poll
    "m s" 'notmuch-search
    "m u" 'notmuch-unread)
  (local-leader-def
    :keymaps 'notmuch-search-mode-map
    "o" #'sulami/notmuch-search-open-pr)
  :hook
  (notmuch-message-mode . flyspell-mode))

Verb

Verb is an extension to org-mode to run HTTP requests in a literate manner. It has all kinds of useful features to build a request library which can be programmed.

(use-package verb
  :defer t
  :general
  (local-leader-def
    :keymaps 'org-mode-map
    "h" '(:ignore t :wk "http")
    "h s" 'verb-send-request-on-point-other-window-stay
    "h S" 'verb-send-request-on-point-other-window
    "h q" 'verb-send-request-on-point-no-window
    "h r" 'verb-re-send-request
    "h v" 'verb-set-var
    "h y" 'verb-export-request-on-point)
  (general-nmap
    :keymaps 'verb-response-body-mode-map
    "q" 'verb-kill-response-buffer-and-window))

Literate Calc Mode

This is my own package, which does live inline calculations. Quite handy, if you ask me.

(use-package literate-calc-mode
  :defer t)

JIRA

Use the builtin bug-reference-mode to automatically convert JIRA ticket references into clickable links. Adapted from monotux.

(use-package bug-reference
  :straight nil
  :config
  (defun sulami/bug-reference-setup ()
    (setq bug-reference-bug-regexp (rx (group
                                        (group
                                         (or "CIRCLE"
                                             "BACKPLANE"
                                             "SRE"
                                             "SERVER"
                                             "INFRA"
                                             "PIPE"
                                             "ORCH"
                                             "PER"
                                             "API"
                                             "TASKS")
                                         "-"
                                         (repeat 3 5 digit))))
          bug-reference-url-format "https://circleci.atlassian.net/browse/%s"))
  :hook
  ((bug-reference-mode . sulami/bug-reference-setup)
   (org-mode . bug-reference-mode)
   (clojure-mode . bug-reference-mode)))

vterm

vterm is a terminal emulator which is way faster than anything built into Emacs. It copes well with huge amounts of output and weird escape sequences. Prior to installation run:

brew install cmake libtool
(use-package vterm
  :defer t
  :commands (vterm)
  :general
  (leader-def
    "a t" 'vterm))

LSP

LSP (Language Server Protocol) is a system which enables static analysis of code and provides various IDE-like features.

(defun sulami/eldoc-documentation-adaptive ()
  "An `eldoc-documentation-functions' function which uses the
-default variant by default to keep the minibuffer small by only
showing signatures, but uses -enthusiast if the *eldoc* buffer is
open."
  (if (eldoc--echo-area-prefer-doc-buffer-p t)
      (eldoc-documentation-enthusiast)
    (eldoc-documentation-default)))

(use-package eglot
  :custom
  (eldoc-echo-area-display-truncation-message nil)
  (eldoc-echo-area-prefer-doc-buffer t)
  (eldoc-documentation-strategy sulami/eldoc-documentation-adaptive)
  :general
  (local-leader-def 'eglot-mode-map
    "l" '(:ignore t :wk "lsp")
    "l r" #'eglot-rename))

Tree-sitter

Tree-sitter is a library for parsing source code, which can be used to generate faster and better syntax highlighting than the traditional regexp-based approach.

(use-package tree-sitter
  :hook
  ((after-init . global-tree-sitter-mode)
   (tree-sitter-after-on . tree-sitter-hl-mode)))

(use-package tree-sitter-langs)

Clojure

Clojure is the language I work with at $DAYJOB, so it has quite a lot of configuration.

(use-package clojure-mode
  :defer t
  :general
  (local-leader-def
    :keymaps 'clojure-mode-map
    "R" '(:ignore t :wk "refactor")
    "R a" 'clojure-align
    "R l" 'clojure-move-to-let
    "R t" 'clojure-thread-first-all
    "R T" 'clojure-thread-last-all
    "R u" 'clojure-unwind-all)
  :config
  (define-clojure-indent
    (database/speculate 1)
    (car/wcar 1)))

(use-package flycheck-clj-kondo
  :defer t
  :hook (clojure-mode . (lambda () (require 'flycheck-clj-kondo))))

CIDER

The big IDE-like integration for Clojure.

(use-package cider
  :defer t
  :config
  (defun sulami/cider-debug-defun-at-point ()
    "Set an implicit breakpoint and load the function at point."
    (interactive)
    (let ((current-prefix-arg '(4)))
      (call-interactively 'cider-eval-defun-at-point)))
  :custom
  (cider-show-error-buffer nil)
  (cider-repl-display-help-banner nil)
  (cider-redirect-server-output-to-repl nil)
  :general
  (local-leader-def
    :keymaps 'cider-mode-map
    "c" 'cider-connect
    "j" 'cider-jack-in
    "q" 'cider-quit
    "s" 'cider-scratch
    "x" 'cider-ns-reload-all
    "e" '(:ignore t :wk "eval")
    "e b" 'cider-eval-buffer
    "e d" 'sulami/cider-debug-defun-at-point
    "e e" 'cider-eval-last-sexp
    "e f" 'cider-eval-defun-at-point
    "e r" 'cider-eval-region
    "h" '(:ignore t :wk "help")
    "h a" 'cider-apropos
    "h A" 'cider-apropos-documentation
    "h d" 'cider-doc
    "h i" 'cider-inspect-last-result
    "h w" 'cider-clojuredocs
    "h W" 'cider-clojuredocs-web
    "r" '(:ignore t :wk "repl")
    "r f" 'cider-insert-defun-in-repl
    "r n" 'cider-repl-set-ns
    "r r" 'cider-switch-to-repl-buffer
    "t" '(:ignore t :wk "test")
    "t b" 'cider-test-show-report
    "t f" 'cider-test-rerun-failed-tests
    "t l" 'cider-test-run-loaded-tests
    "t n" 'cider-test-run-ns-tests
    "t p" 'cider-test-run-project-tests
    "t t" 'cider-test-run-test)
  :hook
  ((clojure-mode . cider-mode)))

HugSQL

HugSQL is a Clojure ORM which adds some special syntax to SQL.

This declaration is only here for config encapsulation, it doesn’t actually install a package.

In this case we install a fix to make imenu work in HugSQL files.

(use-package hugsql
  :straight nil
  :defer t
  :init
  (defun sulami/init-hugsql-imenu ()
    (when (string-suffix-p ".hug.sql" (buffer-file-name))
      (setq
       imenu-generic-expression
       '((nil "^--[[:space:]]:name[[:space:]]+\\([[:alnum:]-]+\\)" 1)))))
  :hook
  (sql-mode . sulami/init-hugsql-imenu))

Common Lisp

One of my favourite recreational languages. I use sly instead of slime because it’s supposedly better. I don’t have much of an opinion, it works.

(use-package sly
  :defer t
  :commands (sly)
  :custom
  (inferior-lisp-program "sbcl")
  :general
  (local-leader-def
    :keymaps 'sly-mode-map
    "s" 'sly
    "q" 'sly-quit-lisp
    "e" '(:ignore t :wk "eval")
    "e b" 'sly-eval-buffer
    "e e" 'sly-eval-last-expression
    "e f" 'sly-eval-defun
    "e r" 'sly-eval-region
    "h" '(:ignore t :wk "help")
    "h a" 'sly-apropos
    "h d" 'sly-documentation
    "h i" 'sly-inspect))

Racket

I use Racket for quite a bit of scripting, due to its rich standard library. I don’t need any fancy configuration though, the major mode comes with reasonable defaults. This does not have the usual eval bindings, because I usually don’t use Racket REPLs.

(use-package racket-mode
  :defer t)

Haskell

I used to write a lot more Haskell, but I still have some. The major mode has reasonable defaults, so that’s all I need for now.

(use-package haskell-mode
  :defer t)

Rust

I don’t actually really use Rust beyond experiments, so not much config here.

(use-package rustic
  :defer t
  :after (rust-mode flycheck)
  :init
  (add-to-list 'flycheck-checkers 'rustic-clippy)
  :config
  (setq rustic-format-on-save t)
  (setq rustic-lsp-client 'eglot)
  (setq rustic-rustfmt-args "--edition 2021")
  :general
  (local-leader-def
    :keymaps 'rustic-mode-map
    "c" #'rustic-compile
    "r" #'rustic-cargo-run
    "t" #'rustic-cargo-test-dwim
    "T" #'rustic-cargo-run-nextest))

(use-package flycheck-rust
  :defer t
  :after (flycheck)
  :hook
  (flycheck-mode . flycheck-rust-setup))

Go

Every now and then I do have to look at some Go code, sadly.

(use-package go-mode
  :defer t
  :custom
  (gofmt-show-errors 'echo)
  :hook
  (before-save . gofmt-before-save))

Web

(use-package web-mode
  :defer t
  :custom
  (js-indent-level 2)
  (web-mode-code-indent-offset 2)
  (web-mode-markup-indent-offset 2)
  (web-mode-script-padding 0)
  (web-mode-style-padding 0)
  :mode
  (("\\.tsx" . web-mode)
   ("\\.vue" . web-mode)))

Typescript

(use-package typescript-mode
  :defer t
  :custom
  (typescript-indent-level 2))

(use-package tide
  :defer t
  :config
  (defun sulami/tide-setup ()
    (when (equal "tsx"
                 (file-name-extension buffer-file-name))
      (tide-setup)
      (flycheck-mode +1)
      (eldoc-mode +1)))
  :hook
  ((typescript-mode . sulami/tide-setup)
   (web-mode . sulami/tide-setup)))

(use-package prettier-js
  :defer t
  :after (nix-sandbox)
  :config
  (defun sulami/prettier-js-setup ()
    (setq prettier-js-command
          (nix-executable-find (nix-current-sandbox)
                               "prettier"))
    (prettier-js-mode +1))
  :hook
  ((typescript-mode . sulami/prettier-js-setup)
   (web-mode . sulami/prettier-js-setup)))

Ruby

The primary language at $DAYJOB. rspec-mode adds imenu support for RSpec.

(use-package rspec-mode
  :general
  (local-leader-def
    :keymaps 'rspec-mode-map
    "t" #'rspec-verify-single
    "T" #'rspec-verify-all))

CSV

(use-package csv-mode
  :defer t)

SQL

I’m using the builtin SQL-mode, but I want it set to highlight for PostgreSQL specifically.

(use-package sql
  :straight nil
  :custom
  (sql-product 'postgres))

Docker

(use-package dockerfile-mode
  :defer t)

Kubernetes

(use-package kubel)

Terraform

(use-package terraform-mode
  :defer t)

Markdown

I think it’s vastly inferior to org-mode, but I still have to use it. Mostly just try to make it look & work like org-mode.

(use-package markdown-mode
  :defer t
  :custom
  (markdown-fontify-code-blocks-natively t)
  :hook
  ((markdown-mode . orgtbl-mode)
   (markdown-mode . flyspell-mode))
  :general
  (local-leader-def
    :keymaps 'markdown-mode-map
    "l" 'markdown-insert-link
    "m" 'markdown-toggle-markup-hiding)
  :mode (("README\\.md\\'" . gfm-mode)
         ("\\.md\\'" . markdown-mode)))

(use-package edit-indirect
  :defer t)

YAML

Sometimes I’m a YAML-engineer, too.

(use-package yaml-mode
  :defer t
  :config
  (defun sulami/yaml-setup ()
    (setq evil-shift-width 2))
  :hook
  (yaml-mode . sulami/yaml-setup))

Nix

There are two distinct parts here:

nix-mode
Provides support for editing Nix expressions
nix-sandbox
Used to make other commands Nix-aware
(use-package nix-mode
  :defer t)

(use-package nix-sandbox
  :disabled)

Protobuf

We use quite a lot of protobuffers at $DAYJOB, so the mode is useful.

(use-package protobuf-mode
  :defer t
  :init
  (defun sulami/init-protobuf-imenu ()
    "Sets up imenu support for Protobuf.

Stolen from Spacemacs."
    (setq
     imenu-generic-expression
     '((nil "^[[:space:]]*\\(message\\|service\\|enum\\)[[:space:]]+\\([[:alnum:]]+\\)" 2))))
  :hook
  (protobuf-mode . sulami/init-protobuf-imenu))

Copilot

I feel slightly weird about this, but it’s essentially supercharged completion, and can often save me a lot of time.

(use-package copilot
  :straight (:host github :repo "zerolfx/copilot.el" :files ("dist" "*.el"))
  :ensure t
  :custom
  (copilot-idle-delay 1)
  :general
  (general-imap
    :keymaps 'copilot-mode-map
    "C-j" #'copilot-complete
    "C-<return>" #'copilot-accept-completion)
  :hook
  (prog-mode . copilot-mode))