Skip to content
master
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 

This is my Emacs configuration, assembled over the course of more than ten years. I first started using Emacs in 2009 out of sheer necessity—I first learned to code using TextMate, but that wasn’t an option upon, after arriving at college, being required to SSH into some creaky Sun boxes running geologically-ancient versions of Solaris. As of this writing, it is 2020, some eleven years into my Emacs journey, and I have an incurable case of Emacs-induced brain worms; I’ve spent on the order of hundreds of hours tweaking and refining my configuration. This file is the result of that process.

If there’s anything that convinced me to take on this lifelong Emacs habit, it’s because both macOS and iOS come, out of the box, with support for Emacs keybindings in their text areas. Said keybindings aren’t hugely exhaustive, but they’re useful, especially given that the Ctrl-k and Ctrl-y inputs for copy and paste operate on a different pasteboard than do ⌘C and ⌘V. Once I discovered that, and the dorkily exhiliating feeling of juggling information through the various system pasteboards, I was more or less entirely beholden to the Emacs way, despite the merits of modal editingr[fn:1]. But the forcing factor was, after I left Apple, arriving at a job where I had to test C code on macOS, Linux, FreeBSD, and SmartOS. The difficulty of remembering keyboard shortcuts between TextMate, Sublime Text, and Emacs was leagues beyond insupportable, and as such I took the Emacs plunge.

This file is an Org-mode document, like all the posts on my blog. And if you’re reading it on my blog in HTML form, it is because my blog pulls in my Emacs configuration as a submodule, and a conveniently-placed symlink means that it is treated like any other post, and its embedded code is rendered in fancy code blocks. If you’re reading it on its GitHub repository, you’ll see it rendered inline in the repository, as its filename is readme.org. And at Emacs boot time, Emacs itself runs org-babel, generates elisp code out of the code snippets embedded herein, and loads it; further updates to the blog entry are a git submodule command away. I feel both delight and shame in equal quantities at this state of affairs: having a really cherried-out, custom-built[fn:2] Emacs setup is one of those things that’s cool because of how uncool it is, like growing giant vegetables at a county fair, or being really good at Connect Four.

Yet I don’t feel bad about publishing something this self-indulgent. Not only because maybe it’ll demystify what the care and feeding of a very-intense Emacs setup looks like, and also because yak-shaving is an excellent way to occupy myself in quarantine [fn:3]. Furthermore, I’m happier with my setup as an org-mode document, as Emacs configurations generally have a high propensity for entropy: it’s easy to leave a few computer-specific hacks in some dusty corner of a config, and then, upon returning years later, having absolutely no idea what that hack does. Forcing a literate style guilts me into documenting every inch of it. In addition, I have been thanked by strangers for the quality of my config, and my coworker described it as “inspiring”, which seems like sufficient justification to continue, at least with respect to my ego.

[fn:1] I’ve tried to reconfigure my brain to use modal editing, to little avail, but the its model of a domain-specific-language for text editing is a hugely exciting one to me.

[fn:2] My configuration is not built atop one of the all-in-one Emacs distributions like Spacemacs or Doom Emacs. I probably would have if either had been around at the beginning of my Emacs journey, but at this point my own personal set of key bindings is burnt into my brain.

[fn:3] Hello, future generations! If you’re reading this, please believe me when I say that :2020 was a truly enervating time to be a human being.

Configuration: start!

Preliminaries

Many of Emacs’s defaults are ill-suited for my purposes, but the first one that needs fixing is the shockingly low garbage-collection threshold, which defaults to a paltry :8kb. Setting it to :100mb seems to strike a nice balance between GC pauses and performance. I also like to turn on the messages associated therewith, since thrashing GC is a good indicator of runaway processes or similar weirdness.

(setq gc-cons-threshold 100000000)

We use use-package everywhere (it is loaded in the ~init.el~ that bootstraps this whole enterprise), save for packages already built into Emacs, and by default we want it to install all mentioned packages. There are some bugs associated with this approach, I am told, but it tends to work for me, and if I get a message about a missing package I just call package-install.

(setq use-package-always-ensure t)

Don’t copy this to your config! This is to work around weird bugs with JITted Emacs.

(setq package-check-signature nil)

OS-specific concerns

Since subsequent packages like libgit may depend on executables like cmake, we need to ensure that Emacs has access to the PATH associated with the current environment.

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

There appears to be some sort of kerfuffle regarding ELPA’s GPG keys. The details are beyond me, but installing this package silences associated warnings.

(use-package gnu-elpa-keyring-update)

Fixing defaults

Fixing Emacs’s defaults is a nontrivial problem. We’ll start with UI concerns.

(setq
 ;; No need to see GNU agitprop.
 inhibit-startup-screen t
 ;; No need to remind me what a scratch buffer is.
 initial-scratch-message nil
 ;; Double-spaces after periods is morally wrong.
 sentence-end-double-space nil
 ;; Never ding at me, ever.
 ring-bell-function 'ignore
 ;; Prompts should go in the minibuffer, not in a GUI.
 use-dialog-box nil
 ;; Fix undo in commands affecting the mark.
 mark-even-if-inactive nil
 ;; Let C-k delete the whole line.
 kill-whole-line t
 ;; search should be case-sensitive by default
 case-fold-search nil
 ;; no need to prompt for the read command _every_ time
 compilation-read-command nil
 ;; don't scroll
 compilation-scroll-output nil
 ;; my source directory
 default-directory "~/src/")

;; Never mix tabs and spaces. Never use tabs, period.
;; We need the setq-default here because this becomes
;; a buffer-local variable when set.
(setq-default indent-tabs-mode nil)

(defalias 'yes-or-no-p 'y-or-n-p)       ; Accept 'y' in lieu of 'yes'.

It’s good that Emacs supports the wide variety of file encodings it does, but UTF-8 should always, always be the default.

(set-charset-priority 'unicode)
(prefer-coding-system 'utf-8-unix)

We also need to turn on a few modes to have behavior that’s even remotely modern.

(delete-selection-mode t)
(global-display-line-numbers-mode t)
(column-number-mode)

Emacs 27 comes with fast current-line highlight functionality, but it can produce some visual feedback in vterm or ivy buffers, so we only activate it in programming or text modes.

(require 'hl-line)
(add-hook 'prog-mode-hook #'hl-line-mode)
(add-hook 'text-mode-hook #'hl-line-mode)
(set-face-attribute 'hl-line nil :background "gray21")

Emacs is super fond of littering filesystems with backups and autosaves, since it was built with the assumption that multiple users could be using the same Emacs instance on the same filesystem. This was valid in 1980. It is no longer the case.

(setq
 make-backup-files nil
 auto-save-default nil
 create-lockfiles nil)

By default, Emacs stores any configuration you make through its UI by writing custom-set-variables invocations to your init file, or to the file specified by custom-file. Though this is convenient, it’s also an excellent way to cause aggravation when the variable you keep trying to modify is being set in some custom-set-variables invocation. We can disable this by mapping it to the null device.

(setq custom-file null-device)

However, because Emacs stores theme-safety information in that file, we have to disable the warnings entirely. This is not particularly secure, but if someone has uploaded malicious code to MELPA inside a theme, I have bigger problems. (Besides, Emacs is not a secure system, and I see no need to try overmuch to make it one.)

(setq custom-safe-themes t)

By default, the list of recent files gets cluttered up with the contents of downloaded packages. It comes with Emacs, so there’s no use-package call required.

(require 'recentf)
(add-to-list 'recentf-exclude "\\elpa")

Emoji don’t work on Emacs versions < 27 (aside from the Mitsuharu Yamamoto emacs-mac port), and for those greater than 27 we seem to need this. A fun fact about this: that commented parenthesis is to work around an inscrutable parsing bug associated with the < in version< that breaks the elisp indentation mode. Emacs!

(if ( version< "27.0" emacs-version ) ; )
    (set-fontset-font "fontset-default" 'unicode "Apple Color Emoji" nil 'prepend)
  (warn "This Emacs version is too old to properly support emoji."))

There are a great many keybindings that are actively hostile, in that they are bound to useless or obsolete functions that are really easy to trigger accidentally.

(unbind-key "C-x C-f") ;; find-file-read-only
(unbind-key "C-x C-d") ;; list-directory
(unbind-key "C-z") ;; suspend-frame
(unbind-key "M-o") ;; facemenu-mode
(unbind-key "<mouse-2>") ;; pasting with mouse-wheel click
(unbind-key "<C-wheel-down>") ;; text scale adjust
(unbind-key "<C-wheel-up>") ;; ditto
(unbind-key "s-n") ;; make-frame

By default, I want paste operations to indent their results. I could express this as defadvice around the yank command, but I try to avoid such measures if possible.

(defun pt-yank ()
  "Call yank, then indent the pasted region, as TextMate does."
  (interactive)
  (let ((point-before (point)))
    (when mark-active (call-interactively 'delete-backward-char))
    (yank)
    (indent-region point-before (point))))

(bind-key "C-y" #'pt-yank)
(bind-key "s-v" #'pt-yank)
(bind-key "C-Y" #'yank)

The out-of-the-box treatment of whitespace is unfortunate, but fixable.

(add-hook 'before-save-hook #'delete-trailing-whitespace)
(setq require-final-newline t)

Emacs instances started outside the terminal do not pick up ssh-agent information unless we use keychain-environment. Note to self: if you keep having to enter your keychain password on macOS, make sure this is in .ssh/config:

Host *
  UseKeychain yes
(use-package keychain-environment
  :config
  (keychain-refresh-environment))

Emacs is also in love with showing you its NEWS file; it’s bound to like four different keybindings. Overriding the function makes it a no-op. You might say… no news is good news. For that matter, we can elide more GNU agitprop.

(defalias 'view-emacs-news 'ignore)
(defalias 'describe-gnu-project 'ignore)

Undo has always been problematic for me in Emacs. The beauty of undo-tree is that it means that, once you’ve typed something into a buffer, you’ll always be able to get it back. At least in theory. undo-tree has long-standing data loss bugs that are unlikely to be fixed. But no other package provodes a comparable experience.

(use-package undo-tree
  :diminish
  :bind (("C-c _" . undo-tree-visualize))
  :config
  (global-undo-tree-mode +1)
  (unbind-key "M-_" undo-tree-map))

I define a couple of my own configuration variables with defvar, and no matter how many times I mark the variable as safe, it warns me every time I set it in the .dir-locals file. Disabling these warnings is probably (?) the right thing to do.

(setq enable-local-variables :all)

Visuals

Emacs looks a lot better when it has a modern monospaced font and VSCode-esque icons.

(ignore-errors (set-frame-font "SF Mono-12"))

(use-package all-the-icons)

(use-package all-the-icons-dired
  :after all-the-icons
  :hook (dired-mode . all-the-icons-dired-mode))

Every Emacs window should, by default occupy all the screen space it can.

(add-to-list 'default-frame-alist '(fullscreen . maximized))

Window chrome both wastes space and looks unappealing.

(when (window-system)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  (tooltip-mode -1))

I use the Doom Emacs themes, which are gorgeous.

(use-package doom-themes
  :config
  (let ((chosen-theme 'modus-vivendi))
    (doom-themes-visual-bell-config)
    (doom-themes-org-config)
    (setq doom-challenger-deep-brighter-comments t
          doom-challenger-deep-brighter-modeline t
          doom-dark+-blue-modeline nil)
    (load-theme chosen-theme)))

Most major modes pollute the modeline, so we pull in diminish.el to quiesce them.

(use-package diminish
  :config
  (diminish 'eldoc-mode)
  (diminish 'visual-line-mode))

The default modeline is pretty uninspiring, and nano-modeline is very minimal and pleasing.

(use-package nano-modeline
  :config (nano-modeline-mode)
  :custom (nano-modeline-position 'bottom))

I find it useful to have a slightly more apparent indicator of which buffer is active at the moment.

(use-package dimmer
  :custom (dimmer-fraction 0.3)
  :config (dimmer-mode))

Highlighting the closing/opening pair associated with a given parenthesis is essential. Furthermore, parentheses should be delimited by color. I may be colorblind, but it’s good enough, usually.

(show-paren-mode)

(use-package rainbow-delimiters
  :hook ((prog-mode . rainbow-delimiters-mode)))

It’s nice to have the option to center a window, given the considerable size of my screen.

(use-package centered-window
  :ensure t
  :custom
  (cwm-centered-window-width 180))

Tree-sitter

As part of my day job, I hack on the ~tree-sitter~ parsing toolkit. Pleasingly enough, the parsers generated by tree-sitter can be used to spruce up syntax highlighting within Emacs: for example, highlighting Python with emacs-tree-sitter will correctly highlight code inside format strings, which is really quite useful.

(use-package tree-sitter
  :hook ((ruby-mode . tree-sitter-hl-mode)
         (js-mode . tree-sitter-hl-mode)
         (rust-mode . tree-sitter-hl-mode)
         (c-mode . tree-sitter-hl-mode)
         (typescript-mode . tree-sitter-hl-mode)
         (go-mode . tree-sitter-hl-mode)))
(use-package tree-sitter-langs)

Tabs

The long-awaited Emacs 27 support for native tabs is shaky, both visually and in terms of functionality. As such, centaur-tabs is the best way to simulate a conventional tabs setup, in which tab sets are grouped by the toplevel project working directory.

(use-package centaur-tabs
  :config
  (centaur-tabs-mode t)
  :custom
  (centaur-tabs-gray-out-icons 'buffer)
  (centaur-tabs-style "rounded")
  (centaur-tabs-height 32)
  (centaur-tabs-set-icons t)
  (centaur-tabs-set-modified-marker t)
  (centaur-tabs-modified-marker "")
  ;; (centaur-tabs-buffer-groups-function #'centaur-tabs-projectile-buffer-groups)

  :bind
  (("s-{" . #'centaur-tabs-backward)
   ("s-}" . #'centaur-tabs-forward)))

Text manipulation

Any modern editor should include multiple-cursor support. Sure, keyboard macros would suffice, sometimes. Let me live. I haven’t yet taken advantage of many of the multiple-cursors commands. Someday.

(use-package multiple-cursors
  :bind (("C-c M m" . #'mc/edit-lines )
         ("C-c M d" . #'mc/mark-all-dwim )))

The fill-paragraph (M-q) command can be useful for formatting long text lines in a pleasing matter. I don’t do it in every document, but when I do, I want more columns than the default :70.

(setq fill-column 135)

Textmate-style tap-to-expand-into-the-current-delimiter is very useful and curiously absent.

(use-package expand-region
  :bind (("C-c n" . er/expand-region)))

Emacs’s keybinding for comment-dwim is M-;, which is not convenient to type or particularly mnemonic outside of an elisp context (where commenting is indeed ;). Better to bind it somewhere sensible.

(bind-key "C-c /" #'comment-dwim)

avy gives us fluent jump-to-line commands mapped to the home row.

(use-package avy
  :bind (("C-c l" . avy-goto-line)
         ("C-c j" . avy-goto-char)))

(use-package ivy-avy)

iedit gives us the very popular idiom of automatically deploying multiple cursors to edit all occurrences of a particular word.

(use-package iedit)

Parenthesis matching is one of the flaws in my Emacs setup as of this writing. I know that there are a lot of options out there—~paredit~, smartparens, etc.—but I haven’t sat down and really capital-L Learned a better solution than the TextMate-style bracket completion (which Emacs calls, somewhat fancifully, ‘electric’).

(electric-pair-mode)

I got used to a number of convenient TextMate-style commands.

(defun pt/eol-then-newline ()
  "Go to end of line, then newline-and-indent."
  (interactive)
  (move-end-of-line nil)
  (newline-and-indent))

(bind-key "s-<return>" #'pt/eol-then-newline)

Quality-of-life improvements

We start by binding a few builtin commands to more-convenient keystrokes.

(defun pt/split-window-thirds ()
  "Split a window into thirds."
  (interactive)
  (split-window-right)
  (split-window-right)
  (balance-windows))

(bind-key "C-c 3" #'pt/split-window-thirds)

Given how often I tweak my config, I bind C-c e to take me to my config file.

(defun open-init-file ()
  "Open this very file."
  (interactive)
  (find-file "~/.config/emacs/readme.org"))

(bind-key "C-c e" #'open-init-file)

Standard macOS conventions would have s-w close the current buffer, not the whole window.

(bind-key "s-w" #'kill-this-buffer)

Ibuffer mode is a dired-esque package for managing buffers. It allows convenient selection of multiple buffers at the time for killing/reorganization purposes. I don’t really know how to use it, but it’s pretty useful.

(bind-key "C-c B" #'ibuffer)

Emacs makes it weirdly hard to just, like, edit a file as root, probably due to supporting operating systems not built on sudo. Enter the sudo-edit package.

(use-package sudo-edit)

By default, Emacs uses a new buffer for every directory you visit in dired. This is not only terrible from a UI perspective—Emacs warns you if you try to use the sensible behavior (the a key, dired-find-alternate-file). Willfully obtuse. The dired+.el library fixes this, but because it’s not on MELPA, I refuse to use it out of principle (this man’s refusal to compromise is nothing short of crankery, and it would be intellectually remiss of me to abet his crankery). But, as always, we can make Emacs do the right thing. Manually. Furthermore, dired complains every time it’s opened on Darwin, so we should fix that.

(defun dired-up-directory-same-buffer ()
  "Go up in the same buffer."
  (find-alternate-file ".."))

(defun my-dired-mode-hook ()
  (put 'dired-find-alternate-file 'disabled nil) ; Disables the warning.
  (define-key dired-mode-map (kbd "RET") 'dired-find-alternate-file)
  (define-key dired-mode-map (kbd "^") 'dired-up-directory-same-buffer))

(add-hook 'dired-mode-hook #'my-dired-mode-hook)

(setq dired-use-ls-dired nil)

Emacs has problems with very long lines. so-long detects them and takes appropriate action. Good for minified code and whatnot.

(global-so-long-mode)

I’ve never needed a font panel in Emacs, not even once.

(unbind-key "s-t")

It’s genuinely shocking that there’s no “duplicate whatever’s marked” command built-in.

(use-package duplicate-thing
  :init
  (defun my-duplicate-thing ()
    "Duplicate thing at point without changing the mark."
    (interactive)
    (save-mark-and-excursion (duplicate-thing 1))
    (call-interactively #'next-line))
  :bind (("C-c u" . my-duplicate-thing)
         ("C-c C-u" . my-duplicate-thing)))

We need to support reading large blobs of data for LSP’s sake.

(setq read-process-output-max (* 1024 1024)) ; 1mb

When I hit, accidentally or purposefully, a key chord that forms the prefix of some other chords, I want to see a list of possible completions and their info.

(use-package which-key
  :diminish
  :custom
  (which-key-setup-side-window-bottom)
  (which-key-enable-extended-define-key t)
  :config
  (which-key-mode)
  (which-key-setup-minibuffer))
(defun display-startup-echo-area-message ()
  "Override the normally tedious startup message."
  (message "Welcome back."))

These libraries are helpful to have around when writing little bits of elisp.

(use-package s)
(use-package dash)

Buffer management

I almost always want to default to a two-buffer setup.

(defun revert-to-two-windows ()
  "Delete all other windows and split it into two."
  (interactive)
  (delete-other-windows)
  (split-window-right))

(bind-key "C-x 1" #'revert-to-two-windows)
(bind-key "C-x !" #'delete-other-windows) ;; Access to the old keybinding.

keyboard-quit doesn’t exit the minibuffer, so I give abort-recursive-edit, which does, a more convenient keybinding.

(bind-key "s-g" #'abort-recursive-edit)

Ivy makes kill-buffer give you a list of possible results, which isn’t generally what I want.

(defun kill-this-buffer ()
  "Kill the current buffer."
  (interactive)
  (kill-buffer nil)
  )

(bind-key "C-x k" #'kill-this-buffer)
(bind-key "C-x K" #'kill-buffer)

Also, it’s nice to be able to kill all buffers.

(defun kill-all-buffers ()
  "Close all buffers."
  (interactive)
  ;; (maybe-unset-buffer-modified)
  (delete-other-windows)
  (save-some-buffers)
  (let
      ((kill-buffer-query-functions '())
       (lsp-restart 'ignore))
    (mapc 'kill-buffer (buffer-list))))

(bind-key "C-c K" #'kill-all-buffers)

VS Code has a great feature where you can just copy a filename to the clipboard. We can write it in a more sophisticated manner in Emacs, which is nice.

(defun copy-file-name-to-clipboard (do-not-strip-prefix)
  "Copy the current buffer file name to the clipboard. The path will be relative to the project's root directory, if set. Invoking with a prefix argument copies the full path."
  (interactive "P")
  (letrec
      ((fullname (if (equal major-mode 'dired-mode) default-directory (buffer-file-name)))
       (relname (file-relative-name fullname (projectile-project-root)))
       (should-strip (and (projectile-project-root) (not do-not-strip-prefix)))
       (filename (if should-strip relname fullname)))
    (kill-new filename)
    (message "Copied buffer file name '%s' to the clipboard." filename)))

(bind-key "C-c p" #'copy-file-name-to-clipboard)

Normally I bind other-window to C-c ,~, but on my ultra-wide-screen monitor, which supports up to 8 buffers comfortably, holding that key to move around buffers is kind of a drag. Some useful commands to remember here are ~aw-ignore-current and aw-ignore-on.

(use-package ace-window
  :config
  ;; Show the window designators in the modeline.
  (ace-window-display-mode)

   ;; Make the number indicators a little larger. I'm getting old.
  (set-face-attribute 'aw-leading-char-face nil :height 2.0 :background "black")

  (defun my-ace-window (args)
    "As ace-window, but hiding the cursor while the action is active."
    (interactive "P")
    (let
        ((cursor-type nil)
         (cursor-in-non-selected-window nil))
      (ace-window nil)))


  :bind (("C-," . my-ace-window))
  :custom
  (aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l) "Designate windows by home row keys, not numbers.")
  (aw-background nil))

Emacs allows you to, while the minibuffer is active, invoke another command that uses the minibuffer, in essence making the minibuffer from a single editing action into a stack of editing actions. In this particular instance, I think it’s appropriate to have it off by default, simply for the sake of beginners who don’t have a mental model of the minibuffer yet. But at this point, it’s too handy for me to discard. Handily enough, Emacs can report your current depth of recursive minibuffer invocations in the modeline.

(setq enable-recursive-minibuffers t)
(minibuffer-depth-indicate-mode)

It’s useful to have a scratch buffer around, and more useful to have a key chord to switch to it.

(defun switch-to-scratch-buffer ()
  "Switch to the current session's scratch buffer."
  (interactive)
  (switch-to-buffer "*scratch*"))

(bind-key "C-c a s" #'switch-to-scratch-buffer)

One of the main problems with Emacs is how many ephemeral buffers it creates. I’m giving popper-mode a try to see if it can stem the flood thereof.

(use-package popper
  :bind (("C-`"   . popper-toggle-latest)
         ("C-c :" . popper-toggle-latest)
         ("C-\\"  . popper-cycle)
         ("C-M-`" . popper-toggle-type))
  :config
  (setq popper-reference-buffers '("\\*Messages\\*"
                              "Output\\*$"
                              "\\*Async Shell Command\\*"
                              help-mode
                              "magit:.\*"
                              "\\*deadgrep.\*"
                              "\\*vterm.\*"
                              "\\*xref\\*"
                              compilation-mode))
  (popper-mode +1)
  (popper-echo-mode +1))

Org-mode

Even though my whole-ass blogging workflow is built around org-mode, I still can’t say that I know it very well. I don’t take advantage of org-agenda, org-timer, org-calendar, org-capture, anything interesting to do with tags, et cetera. Someday I will learn these things, but not yet.

(use-package org
  :pin org
  :hook ((org-mode . visual-line-mode))
  :bind (("C-c o c" . counsel-org-capture)
         ("C-c o a" . org-agenda)
         :map org-mode-map
         ("C-c c" . #'org-mode-insert-code)
         ("C-c a f" . #'org-shifttab)
         ("C-c a S" . #'zero-width))
  :custom
  (org-adapt-indentation nil)
  (org-directory "~/txt")
  (org-default-notes-file (concat org-directory "/notes.org"))
  (org-return-follows-link t)
  (org-src-ask-before-returning-to-edit-buffer nil "org-src is kinda needy out of the box")
  (org-src-window-setup 'split-window-below)
  (org-agenda-files (list (concat org-directory "/agenda.org")))

  :config
  ;; Putting these in a loop or in :bind generates invalid code and I have no idea why.
  (unbind-key "C-," org-mode-map)
  (unbind-key "C-c ;" org-mode-map)
  (unbind-key "C-c ," org-mode-map)
  (unbind-key "M-<left>" org-mode-map)
  (unbind-key "M-<right>" org-mode-map)

  (defun make-inserter (c) '(lambda () (interactive) (insert-char c)))
  (defun zero-width () (interactive) (insert ""))

  (defun org-mode-insert-code ()
    "Like markdown-insert-code, but for org instead."
    (interactive)
    (org-emphasize ?~)))

(use-package org-bullets
  :hook (org-mode . org-bullets-mode))

(use-package org-ref
  :config (defalias 'dnd-unescape-uri 'dnd--unescape-uri))

(use-package org-roam
  :bind
  (("C-c o r" . org-roam-capture)
   ("C-c o f" . org-roam-node-find))
  :custom
  (org-roam-directory (expand-file-name "~/Dropbox/txt/roam"))
  (org-roam-v2-ack t)
  :config
  (org-roam-db-autosync-mode))

Keymacs support

I recently acquired a Keymacs A620N, a reproduction of the Symbolics 365407, from 1983. Though it’s expensive, it’s unquestionably the nicest keyboard I’ve ever used, given its vintage ALPS switches; of the keyboards I’ve used, only the keyboard.io comes close. However, since the keyboard is simply massive, we need to unbind some of the function keys, as I keep hitting them.

(bind-key "<f1>" #'other-window)

IDE features

Magit

Magit is one of the top three reasons anyone should use Emacs. What a brilliant piece of software it is. I never thought I’d be faster with a git GUI than with the command line, since I’ve been using git for thirteen years at this point, but wonders really never cease. Magit is as good as everyone says, and more.

(use-package magit
  :diminish magit-auto-revert-mode
  :diminish auto-revert-mode
  :bind (("C-c g" . #'magit-status))
  :custom
  (magit-repository-directories '(("~/src" . 1)))
  :config
  (add-to-list 'magit-no-confirm 'stage-all-changes))

Pulling in the libgit module makes Magit a good deal faster. For some reason, Emacs has problems determining the correct file extension for the resulting build product; it chooses .so even though Emacs expects a dylib. To fix this, change directory to where the offending module lives and change its file extension to what is expected.

EDIT: this is crashing on the JITted emacs and I don’t know why. Hopefully by the time Emacs 28 comes out they’ll have figured out the dylib situation.

(use-package libgit)

(use-package magit-libgit
  :after (magit libgit))

Magit also allows integration with GitHub and other such forges (though I hate that term).

(use-package forge
  :after magit)

;; hack to eliminate weirdness
(unless (boundp 'bug-reference-auto-setup-functions)
  (defvar bug-reference-auto-setup-functions '()))

Projectile

Most every nontrivial package provides projectile integration in some form or fashion.

(use-package projectile
  :diminish
  :bind (("C-c k" . #'projectile-kill-buffers)
         ("C-c m" . #'projectile-compile-project))
  :custom
  (projectile-completion-system 'ivy)
  (projectile-enable-caching t)
  :config (projectile-mode))

Ivy, counsel, and swiper

Now that Helm And with ivy-rich, things even look nice. counsel provides nice UI chrome for built-in commands, counsel-projectile provides project integration, and amx provides most-recently-used information and keeps track of which commands I use and which I don’t.

(use-package ivy
  :diminish
  :custom
  (ivy-height 30)
  (ivy-use-virtual-buffers t)
  (ivy-use-selectable-prompt t)
  :config
  (ivy-mode 1)

  :bind (("C-c s"   . #'swiper-thing-at-point)
         ("C-s"     . #'swiper)))

(use-package ivy-rich
  :custom
  (ivy-virtual-abbreviate 'full)
  (ivy-rich-switch-buffer-align-virtual-buffer nil)
  (ivy-rich-path-style 'full)
  :config
  (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line)
  (ivy-rich-mode))

(use-package counsel
  :init
  (counsel-mode 1)
  :config

  (defun pt/yank-pop ()
    "As pt/yank, but calling counsel-yank-pop."
    (interactive)
    (let ((point-before (point)))
      (counsel-yank-pop)
      (indent-region point-before (point))))

  :bind (("C-c ;" . #'counsel-M-x)
         ("C-c U" . #'counsel-unicode-char)
         ("C-c i" . #'counsel-imenu)
         ("C-x f" . #'counsel-find-file)
         ("C-c Y" . #'counsel-yank-pop)
         ("C-c y" . #'pt/yank-pop)
         ("C-c r" . #'counsel-recentf)
         ("C-c v" . #'counsel-switch-buffer-other-window)
         ("C-c H" . #'counsel-projectile-rg)
         ("C-h h" . #'counsel-command-history)
         ("C-x C-f" . #'counsel-find-file)
         ("C-x b" . #'counsel-switch-buffer)
         :map ivy-minibuffer-map
         ("C-r" . counsel-minibuffer-history))
  :diminish)

(use-package counsel-projectile
  :bind (("C-c f" . #'counsel-projectile)
         ("C-c F" . #'counsel-projectile-switch-project)))

(use-package amx :config (amx-mode))

Dumb-jump is pretty good at figuring out where declarations of things might be. I’m using it with C because I’m too lazy to set up true C LSP integration.

(use-package dumb-jump
  :bind (("C-c J" . #'dumb-jump-go)))

Flycheck

Flycheck performs in-buffer highlighting of errors and warnings, and is superior on many axes to the builtin flymake mode. The only configuration it needs is to add ~proselint~ support and to disable the documentation checking in org-src buffers.

(use-package flycheck
  :after org
  :hook
  (org-src-mode . disable-flycheck-for-elisp)
  :custom
  (flycheck-emacs-lisp-initialize-packages t)
  (flycheck-display-errors-delay 0.9)
  :config
  (global-flycheck-mode)
  (flycheck-set-indication-mode 'left-margin)

  (defun disable-flycheck-for-elisp ()
    (setq-local flycheck-disabled-checkers '(emacs-lisp-checkdoc)))

  (add-to-list 'flycheck-checkers 'proselint)
  (setq-default flycheck-disabled-checkers '(haskell-stack-ghc)))

(use-package flycheck-inline
  :disabled
  :config (global-flycheck-inline-mode))

Searching

deadgrep is the bee’s knees for project-wide search, as it uses ripgrep.

(use-package deadgrep
  :bind (("C-c h" . #'deadgrep)))

I remember the days before Emacs had real regular expressions. Nowadays, we have them, but the find-and-replace UI is bad. visual-regexp fixes this. I have this bound to an incredibly stupid keybinding because I simply do not want to take the time to catabolize/forget that particular muscle memory.

(use-package visual-regexp
  :bind (("C-c 5" . #'vr/replace)))

Autocomplete

Completion in Emacs is sort of a fraught enterprise, given the existence of pcomplete, hippie-expand, and complete.el. company is the least problematic and most modern of these alternatives, though it’s kind of a bear to configure. Its interface is not so nice by default but all the frontends flicker terribly if you’re typing quickly, which is just spectacularly distracting.

(use-package company
  :diminish
  :bind (("C-." . #'company-capf))
  :bind (:map company-active-map
         ("C-n" . #'company-select-next)
         ("C-p" . #'company-select-previous))
  :hook (prog-mode . company-mode)
  :custom
  (company-dabbrev-downcase nil "Don't downcase returned candidates.")
  (company-show-numbers t "Numbers are helpful.")
  (company-tooltip-limit 20 "The more the merrier.")
  (company-tooltip-idle-delay 0.4 "Faster!")
  (company-async-timeout 20 "Some requests can take a long time. That's fine.")
  (company-idle-delay 1.5 "Default is way too low.")
  :config

  ;; Use the numbers 0-9 to select company completion candidates
  (let ((map company-active-map))
    (mapc (lambda (x) (define-key map (format "%d" x)
                        `(lambda () (interactive) (company-complete-number ,x))))
          (number-sequence 0 9))))

Debugging

In Haskell, my language of choice, I rarely need a step-through debugger, as designs that minimize mutable state make it so printf debugging is usually all you need. (Haskell’s unorthodox evaluation strategy, and its limited step-through debugging facilities, don’t help either.) However, now that I’m writing Rust and Go at work, a step-through debugger is indicated.

(use-package dap-mode
  :bind
  (("C-c b b" . dap-breakpoint-toggle)
   ("C-c b r" . dap-debug-restart)
   ("C-c b l" . dap-debug-last)
   ("C-c b d" . dap-debug))
  :init
  (require 'dap-go)
  ;; NB: dap-go-setup appears to be broken, so you have to download the extension from GH, rename its file extension
  ;; unzip it, and copy it into the config so that the following path lines up
  (setq dap-go-debug-program '("node" "/Users/patrickt/.config/emacs/.extension/vscode/golang.go/extension/dist/debugAdapter.js"))
  :config
  (dap-mode)
  (dap-auto-configure-mode)
  (dap-ui-mode)
  (dap-ui-controls-mode)
  )

LSP

Before Emacs 27, the LSP experience on large projects was not particularly good. We now have native JSON parsing support. I am told that it makes things easier.

(use-package lsp-mode
  :commands (lsp lsp-execute-code-action)
  :hook ((go-mode . lsp-deferred)
         (lsp-mode . lsp-enable-which-key-integration)
         (lsp-mode . lsp-modeline-diagnostics-mode))
  :bind (("C-c C-c" . #'lsp-execute-code-action)
         ("s-[" . #'xref-pop-marker-stack)
         ("s-]" . #'pop-global-mark))
  :custom
  (lsp-diagnostics-modeline-scope :project)
  (lsp-file-watch-threshold 5000)
  (lsp-response-timeout 2)
  (lsp-eldoc-render-all nil)
  (eldoc-idle-delay 2)
  (lsp-enable-file-watchers nil))

(use-package lsp-ui
  :custom
  (lsp-ui-doc-show-with-mouse t)
  (lsp-ui-doc-show-with-cursor nil)
  (lsp-ui-doc-delay 1.5) ;; buggy, see https://github.com/emacs-lsp/lsp-ui/issues/664
  :config
  (lsp-ui-doc-mode)
  :after lsp-mode)

(use-package lsp-ivy
  :after (ivy lsp-mode)
  :bind (("s-t" . #'lsp-ivy-workspace-symbol)))

(use-package company-lsp
  :disabled
  :custom (company-lsp-enable-snippet t)
  :after (company lsp-mode))

Haskell

Haskell is my day-to-day programming language, so I’ve tinkered with it a good deal. Featuring automatic ormolu or stylish-haskell invocation, as based on a per-project variable, so I can default to ormolu but choose stylish-haskell for the projects that don’t.

(use-package haskell-mode

  :config
  (defcustom haskell-formatter 'ormolu
    "The Haskell formatter to use. One of: 'ormolu, 'stylish, nil. Set it per-project in .dir-locals."
    :safe 'symbolp)

  (defun haskell-smart-format ()
    "Format a buffer based on the value of 'haskell-formatter'."
    (interactive)
    (cl-ecase haskell-formatter
      ('ormolu (ormolu-format-buffer))
      ('stylish (haskell-mode-stylish-buffer))
      (nil nil)
      ))


  (defun haskell-switch-formatters ()
    "Switch from ormolu to stylish-haskell, or vice versa."
    (interactive)
    (setq haskell-formatter
          (cl-ecase haskell-formatter
            ('ormolu 'stylish)
            ('stylish 'ormolu)
            (nil nil))))

  :bind (:map haskell-mode-map
         ("C-c a c" . haskell-cabal-visit-file)
         ("C-c a i" . haskell-navigate-imports)
         ("C-c m"   . haskell-compile)
         ("C-c a I" . haskell-navigate-imports-return)
         :map haskell-cabal-mode-map
         ("C-c m"   . haskell-compile)))

(use-package haskell-snippets
  :after (haskell-mode yasnippet)
  :defer)
(use-package lsp-haskell
  :hook (haskell-mode . lsp)
  :custom
  (lsp-haskell-process-path-hie "haskell-language-server-wrapper")
  (lsp-haskell-process-args-hie '())
  )

My statements about Haskell autoformatters have, in the past, attracted controversy, so I have no further comment on the below lines. Note that haskell-lsp runs ormolu with lsp-format-buffer.

(use-package ormolu)

vterm

The state of terminal emulation is, as a whole, a mess. Not just within Emacs, but across all of Unix. (To be fair, terminals are a fascinating study in backwards compatibility and generations upon generations of standards and conventions.) A recent bright spot has been libvterm, which, when integrated with Emacs’s new dynamic module support, enables us to have a very, very fast terminal inside Emacs.

A thing I want to do someday is to write a framework for sending things like compile commands to a running vterm buffer with vterm-send-string. I want a version of the compile command that sends that command to my current vterm buffer. That would be so badass.

(use-package vterm
  :config
  (defun turn-off-chrome ()
    (hl-line-mode -1)
    (display-line-numbers-mode -1))
  :hook (vterm-mode . turn-off-chrome))

(use-package vterm-toggle
  :custom
  (vterm-toggle-fullscreen-p nil "Open a vterm in another window.")
  (vterm-toggle-scope 'project)
  :bind (("C-c t" . #'vterm-toggle)
         :map vterm-mode-map
         ("C-\\" . #'popper-cycle)
         ("s-t" . #'vterm) ; Open up new tabs quickly
         ))

Process management

prodigy is a great and handsome frontend for managing long-running services. Since many of the services I need to run are closed-source, the calls to prodigy-define-service are located in an adjacent file. Unfortunately, prodigy doesn’t really have any good support for managing Homebrew services. Maybe I’ll write one, in my copious spare time.

(use-package prodigy
  :bind (("C-c 8" . #'prodigy)
         :map prodigy-view-mode-map
         ("$" . #'end-of-buffer))
  :custom (prodigy-view-truncate-by-default t)
  :config
  (load "~/.config/emacs/services.el" 'noerror))

Snippets

I grew up writing in TextMate, so I got extremely used to text-expansion snippets. I also think they’re extremely underrated for learning a new language’s idioms: one of the reasons I was able to get up to speed so fast with Rails (back in the 1.2 days) was because the TextMate snippets indicated pretty much everything you needed to know about things like ActiveRecord.

(use-package yasnippet
  :defer 3 ;; takes a while to load, so do it async
  :diminish yas-minor-mode
  :config (yas-global-mode)
  :custom (yas-prompt-functions '(yas-completing-prompt)))

Other Languages

General-purpose

Rust is one of my favorite languages in the world.

(use-package rust-mode
  :hook ((rust-mode . lsp)
         (rust-mode . lsp-lens-mode)
         )
  :custom
  (rust-format-on-save t)
  (lsp-rust-server 'rust-analyzer))

I occasionally write Go, generally as a glue language to munge things together. I find certain aspects of its creators’ philosophies to be repellent, but a language is more than its creators, and it’s hard to argue with the success it’s found in industry or the degree to which people find it easy to pick up.

(use-package go-mode
  :custom
  (lsp-enable-links nil)
  (lsp-ui-doc-mode nil)
  :config
  (add-hook 'before-save-hook #'gofmt-before-save)
  )

(use-package go-snippets)

(defun fix-messed-up-gofmt-path ()
  (interactive)
  (setq gofmt-command (string-trim (shell-command-to-string "which gofmt"))))

(use-package go-projectile)

(use-package gotest
  :bind (:map go-mode-map
              ("C-c a t" . #'go-test-current-test)))

Elm is a good language.

(use-package elm-mode
  :ensure t
  :hook ((elm-mode . elm-format-on-save-mode)
         (elm-mode . elm-indent-mode)))

I don’t write a lot of Python, but when I do I like to use the extremely opinionated black formatter.

(use-package blacken
  :hook ((python-mode . blacken-mode)))

Some other miscellaneous languages that I don’t write often but for which I need syntax highlighting, at least.

(use-package typescript-mode)
(use-package csharp-mode)
(setq-default js-indent-level 2)

I’m trying to learn APL, because I’ve lost control of my life.

(use-package dyalog-mode)

Configuration

(use-package yaml-mode)
(use-package dockerfile-mode)
(use-package toml-mode)

I use Bazel for some Haskell projects.

(use-package bazel
  :config
  (add-hook 'bazel-mode-hook (lambda () (add-hook 'before-save-hook #'bazel-mode-buildifier nil t)))
  )

Interchange

(use-package protobuf-mode)

Markup

I generally use GitHub-flavored Markdown, so we default to that.

(use-package markdown-mode
  :bind (("C-c C-s a" . markdown-table-align))
  :mode ("\\.md$" . gfm-mode))

Occasionally I need to edit Rails .erb templates, God help me.

(use-package web-mode
  :mode ("\\.html.erb$" . web-mode))

I usually use curly quotes when writing in markup languages, which typo-mode makes easy.

(use-package typo)

Shell

(use-package fish-mode)

Miscellany

restclient is a terrific interface for running HTTP requests against local or remote services.

(use-package restclient
  :mode ("\\.restclient$" . restclient-mode))

Dash is the foremost documentation browser for macOS.

(use-package dash-at-point
  :bind ("C-c d" . dash-at-point))

TRAMP mode is excellent for editing files on a remote machine or Docker container, but it needs some TLC.

(require 'tramp)
(setq tramp-default-method "ssh"
      tramp-verbose 1
      tramp-default-remote-shell "/bin/bash"
      tramp-connection-local-default-shell-variables
        '((shell-file-name . "/bin/bash")
          (shell-command-switch . "-c")))

(connection-local-set-profile-variables 'tramp-connection-local-default-shell-profile
  '((shell-file-name . "/bin/bash")
   (shell-command-switch . "-c")))

(lsp-register-client
 (make-lsp-client :new-connection (lsp-stdio-connection "gopls")
                  :major-modes '(go-mode go-dot-mod-mode)
                  :language-id "go"
                  :remote? t
                  :priority 0
                  :server-id 'gopls-remote
                  :completion-in-comments? t
                  :library-folders-fn #'lsp-go--library-default-directories
                  :after-open-fn (lambda ()
                                   ;; https://github.com/golang/tools/commit/b2d8b0336
                                   (setq-local lsp-completion-filter-on-incomplete nil))))
(use-package direnv :config (direnv-mode))

Initial screen setup

(defun my-default-window-setup ()
  "Called by emacs-startup-hook to set up my initial window configuration."

  (split-window-right)
  (other-window 1)
  (find-file "~/txt/todo.org")
  (other-window 1))

(add-hook 'emacs-startup-hook #'my-default-window-setup)

Adios

If you made it this far, well, may your deity of choice bless you. If you don’t use Emacs already, I hope I tempted you a little. If you do, I hope you learned a couple new tricks, just as I have learned so many tricks from reading dozens of other people’s configs.

Au revoir.

(provide 'init)

About

the greatest emacs setup of all time

Resources

Releases

No releases published

Packages

No packages published