Here is my configuration for the almighty emacs editor. I used to think it was long and comprehensive until I saw some of the more prolific configs that abound, so I guess it might be considered somewhat minimalist but I’ve been using emacs for many years and this works for me. The vast majority of my config is just loading packages for working with various programming languages, et cetera, but there are some tweaks included herein. My philosophy is to make few changes, only when there’s a clear value-add, and so this file has been built up incrementally over the course of several years. I believe this is the best way to leverage emacs’ strengths and mold it to yourself.
I do not use `vim`-style bindings because I am not a deranged lunatic.
This is a so-called “literate” code file, where the code and its documentation are intertwined as one file and separated at compile/runtime. This file is the primary configuration artifact and most of the sausage gets made here. There is also an early-init file that sets a few variables before the package system and GUI are loaded.
- Install emacs. Caveat: I primarily use emacs on macOS using the Yamamoto (aka the railwaycat) fork, so your mileage may vary.
- Issue the following commands:
$ git clone [email protected]:nathanvy/dotemacs.git
$ cd .emacs.d/
$ make
First enable lexical binding, then set up [https://github.com/progfolio/elpaca][Elpaca]] which is a replacement for the built-in package.el
functionality, because package.el is a dumpster fire and apparently not even the emacs maintainers know how it works. Note that we use tree-sitter so technically we depend on emacs version 29+, but you could use the tree-sitter packages for prior versions. In that case change/delete the check below.
;; -*- lexical-binding: t; -*-
(error "This emacs requires a min version of 29 but you're running %s" emacs-version))
(setq user-full-name "Nathan Van Ymeren"
user-mail-address "[email protected]")
(setq use-short-answers t)
(tool-bar-mode -1)
(set-default-coding-systems 'utf-8)
(prefer-coding-system 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-language-environment "English")
This is the Elpaca installer. It may need to be updated if it is changed upstream.
(defvar elpaca-installer-version 0.7)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
:ref nil :depth 1
:files (:defaults "elpaca-test.el" (:exclude "extensions"))
:build (:not elpaca--activate-package)))
(let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory))
(build (expand-file-name "elpaca/" elpaca-builds-directory))
(order (cdr elpaca-order))
(default-directory repo))
(add-to-list 'load-path (if (file-exists-p build) build repo))
(unless (file-exists-p repo)
(make-directory repo t)
(when (< emacs-major-version 28) (require 'subr-x))
(condition-case-unless-debug err
(if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
((zerop (apply #'call-process `("git" nil ,buffer t "clone"
,@(when-let ((depth (plist-get order :depth)))
(list (format "--depth=%d" depth) "--no-single-branch"))
,(plist-get order :repo) ,repo))))
((zerop (call-process "git" nil buffer t "checkout"
(or (plist-get order :ref) "--"))))
(emacs (concat invocation-directory invocation-name))
((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
"--eval" "(byte-recompile-directory \".\" 0 'force)")))
((require 'elpaca))
((elpaca-generate-autoloads "elpaca" repo)))
(progn (message "%s" (buffer-string)) (kill-buffer buffer))
(error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
(unless (require 'elpaca-autoloads nil t)
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
Enable Elpaca’s use-package integration and customize use-package.
(elpaca elpaca-use-package (elpaca-use-package-mode)
(setq use-package-always-ensure t))
I rarely if ever use Customize
but when I do I prefer it to put its mess into its own tidy file. Also here I’ll load some secrets that I don’t want in version control.
(setq custom-file "~/.emacs.d/custom.el")
(when (file-exists-p custom-file)
(add-hook 'elpaca-after-init-hook (lambda () (load custom-file))))
(when (file-exists-p (expand-file-name "secrets.el" user-emacs-directory))
(load-file (expand-file-name "secrets.el" user-emacs-directory)))
This sets up a centralized location where emacs will leave its temporary files instead of littering our filesystem.
(setq temporary-file-directory "~/.emacs.d/saves")
(setq backup-directory-alist
`((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
`((".*" ,temporary-file-directory t)))
(message "Deleting old backup files...")
(let ((week (* 60 60 24 7))
(current (float-time (current-time))))
(dolist (file (directory-files temporary-file-directory t))
(when (and (backup-file-name-p file)
(> (- current (float-time (nth 5 (file-attributes file))))
week))
(message "%s" file)
(delete-file file))))
Some utility functions:
(defun increment-number-at-point ()
(interactive)
(skip-chars-backward "0-9")
(or (looking-at "[0-9]+")
(error "No number at point"))
(replace-match (number-to-string (1+ (string-to-number (match-string 0))))))
(defun decrement-number-at-point ()
(interactive)
(skip-chars-backward "0-9")
(or (looking-at "[0-9]+")
(error "No number at point"))
(replace-match (number-to-string (1- (string-to-number (match-string 0))))))
(defun insert-line-below ()
"Insert a blank line below point"
(interactive)
(move-beginning-of-line nil)
(insert "\n")
(if electric-indent-inhibit
(let* ((indent-end (progn (crux-move-to-mode-line-start) (point)))
(indent-start (progn (move-beginning-of-line nil) (point)))
(indent-chars (buffer-substring indent-start indent-end)))
(forward-line -1)
(insert indent-chars))
(forward-line -1)
(indent-according-to-mode)))
Now that we’re bootstrapped we can start pulling in stuff that we use to get other stuff done. We’ll start with some OS-specific stuff:
(when (eq system-type 'darwin)
(customize-set-variable 'native-comp-driver-options '("-Wl,-w")) ;;revisit in emacs 29
(use-package exec-path-from-shell
:config
(exec-path-from-shell-initialize)))
;; (when (eq system-type 'gnu/linux))
And some general utility packages. Transpose-frame lets us move frames around easily, and smex aka Smart M-x is just groovy.
(use-package transpose-frame)
(use-package smex)
(use-package projectile)
(use-package magit)
(use-package which-key
:config
(which-key-mode))
There are lots of competing (or perhaps it would be better to say overlapping) packages in this space but I like good old ido
. It does what I need. ido
is built in but if you actually set `ido-everywhere = 1` you may discover it’s not actually everywhere so we add ido-completing-read+
(setq ido-enable-flex-matching t)
(ido-mode 1)
(ido-everywhere 1)
(use-package ido-completing-read+
:config
(ido-ubiquitous-mode 1))
I stumbled upon prism-mode
by accident after much mucking about with rainbow-delimiters
and friends, and I’ve really come to prefer prism for coloring.
I shopped around for themes quite a bit because emacs by default is quite frankly hideous, and I spent quite some time embracing the glorious 80s aesthetic and for a while enjoyed a super dank synthwave type theme. Originally I had settled on the vscode-dark+
theme which I really liked and heartily recommend but sometimes you want to have more fun. Base16
-based themes also get an honorable mention for being good. Lots of folks use solarized
but I found it didn’t have enough contrast for me. These days I appear to have settled on nord
.
We thank these themes for their prior service:
synthwave-emacs
doom-outrun-electric
doom-laserwave
tomorrow-night
vscode-dark
(column-number-mode t)
(show-paren-mode t)
(setq-default indent-tabs-mode nil)
(use-package nord-theme
:if (display-graphic-p)
:config
(set-face-attribute 'default nil :family "Monaco")
(set-face-attribute 'fixed-pitch nil :family "Monaco")
(set-face-attribute 'variable-pitch nil :family "SF Pro Display" :height 140)
(load-theme 'nord t))
(use-package all-the-icons
:if (display-graphic-p))
(use-package mode-line-bell
:config (mode-line-bell-mode))
;; temporarily disabled
;; (use-package prism
;; :commands prism-mode
;; :init
;; (add-hook 'go-mode-hook #'prism-mode)
;; (add-hook 'csharp-mode-hook #'prism-mode)
;; (add-hook 'js-mode-hook #'prism-mode)
;; (add-hook 'js-jsx-mode-hook #'prism-mode)
;; (add-hook 'typescirpt-mode-hook #'prism-mode)
;; (add-hook 'c++-mode-hook #'prism-mode)
;; (add-hook 'emacs-lisp-mode-hook #'prism-mode)
;; (add-hook 'ielm-mode-hook #'prism-mode)
;; (add-hook 'lisp-mode-hook #'prism-mode)
;; (add-hook 'lisp-interaction-mode-hook #'prism-mode)
;; (add-hook 'scheme-mode-hook #'prism-mode)
;; (add-hook 'python-mode-hook #'prism-whitespace-mode))
Parrot Mode needs no introduction, and no explanation.
(use-package parrot
:if (display-graphic-p)
:config (parrot-mode))
Emacs and LSP together make for a fantastic editing experience and has deprecated a lot of previously-indispensable stuff so we’ll get it going along with company for completion. For pre-29 emacs this is where I also use-package
‘d the tree-sitter packages and languages but with the release of 29.1 that’s no longer necessary as long as you compile emacs --with-tree-sitter
(use-package lsp-mode
:init
;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l")
(setf lsp-keymap-prefix "C-c l")
:hook ((go-ts-mode . (lambda ()
(lsp-go-install-save-hooks)
(lsp)))
(csharp-ts-mode . lsp)
(ess-r-mode . lsp)
(web-mode . lsp)
(js-ts-mode .lsp)
(js-jsx-mode . lsp)
(typescript-ts-mode . lsp)
(c-or-c++-ts-mode . lsp)
(python-ts-mode . (lambda ()
(require 'lsp-python-ms)
(lsp))))
:commands lsp lsp-deferred
:config
(setq lsp-log-io nil))
(use-package lsp-ui
:hook (lsp-mode . lsp-ui-mode))
(use-package flycheck
:init (global-flycheck-mode))
(use-package lsp-treemacs
:commands lsp-treemacs-errors-list)
(use-package company
:hook (prog-mode . company-mode))
Thanks Mickey Petersen for this list, to which I’ve added a few:
;; https://www.masteringemacs.org/article/how-to-get-started-tree-sitter
(setq treesit-language-source-alist
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
(cmake "https://github.com/uyha/tree-sitter-cmake")
(css "https://github.com/tree-sitter/tree-sitter-css")
(csharp "https://github.com/tree-sitter/tree-sitter-c-sharp")
(lisp "https://github.com/theHamsta/tree-sitter-commonlisp")
(cuda "https://github.com/theHamsta/tree-sitter-cuda")
(elisp "https://github.com/Wilfred/tree-sitter-elisp")
(fortran "https://github.com/stadelmanma/tree-sitter-fortran")
(go "https://github.com/tree-sitter/tree-sitter-go")
(html "https://github.com/tree-sitter/tree-sitter-html")
(java "https://github.com/tree-sitter/tree-sitter-java")
(javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
(julia "https://github.com/tree-sitter/tree-sitter-julia")
(json "https://github.com/tree-sitter/tree-sitter-json")
(latex "https://github.com/latex-lsp/tree-sitter-latex")
(lua "https://github.com/Azganoth/tree-sitter-lua")
(make "https://github.com/alemuller/tree-sitter-make")
(markdown "https://github.com/ikatyang/tree-sitter-markdown")
(objc "https://github.com/jiyee/tree-sitter-objc")
(org "https://github.com/milisims/tree-sitter-org")
(perl "https://github.com/tree-sitter-perl/tree-sitter-perl")
(php "https://github.com/tree-sitter/tree-sitter-php")
(proto "https://github.com/mitchellh/tree-sitter-proto")
(python "https://github.com/tree-sitter/tree-sitter-python")
(R "https://github.com/r-lib/tree-sitter-r")
(ruby "https://github.com/tree-sitter/tree-sitter-ruby")
(rust "https://github.com/tree-sitter/tree-sitter-rust")
(scheme "https://github.com/6cdh/tree-sitter-scheme")
(sql "https://github.com/m-novikov/tree-sitter-sql")
(toml "https://github.com/tree-sitter/tree-sitter-toml")
(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
(typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(yaml "https://github.com/ikatyang/tree-sitter-yaml")))
But we don’t want to manually deal with enabling the <name>-ts-mode
and wondering which are available so we’ll just use treesit-auto
:
;;https://github.com/renzmann/treesit-auto
(use-package treesit-auto
:config
(setq treesit-auto-install 'prompt)
(global-treesit-auto-mode))
In 2021 I started writing a lot of Go (golang) and there’s an awful lot of repetitive error checking when trying to follow the idiomatic style. I got annoyed at writing the same if construct hundreds of times so I decided it was finally time to install yasnippet. It comes with TAB
bound to yas-expand
by default which I don’t like, so I disabled it here by setting it to nil, and moved it to a different key combination at the end of this file.
(use-package yasnippet
:init
(yas-global-mode)
(define-key yas-minor-mode-map (kbd "<tab>") nil)
(define-key yas-minor-mode-map (kbd "TAB") nil))
I hated lisp at first but I’ve found that it’s really grown on me. It has its warts but all languages do. We don’t leverage LSP here since most lisp implementations predate Language Servers and provide their own analogous constructs that are more tightly integrated with the REPL anyway. Sly is a fork of SLIME and is more actively developed.
(use-package sly
:config
(setq inferior-lisp-program "sbcl")
(setq org-babel-lisp-eval-fn #'sly-eval)
(setq org-confirm-babel-evaluate nil))
(use-package paredit
:mode "paredit-mode"
:commands enable-paredit-mode
:init
(add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode)
(add-hook 'eval-expression-minibuffer-setup-hook #'enable-paredit-mode)
(add-hook 'ielm-mode-hook #'enable-paredit-mode)
(add-hook 'lisp-mode-hook #'enable-paredit-mode)
(add-hook 'lisp-interaction-mode-hook #'enable-paredit-mode)
(add-hook 'scheme-mode-hook #'enable-paredit-mode))
At the time of writing this paragraph I’m in an MBA program and for our analytics courses they inexplicably chose R over Python, because I guess they hate us. So here’s ess
(Emacs Speaks Statistics). I haven’t bothered to set up polymode
for doing “RMarkdown” shenanigans because org is good enough for me.
(use-package ess
:bind (:map ess-r-mode-map
("M-p" . " %>%"))
:config
(require 'ess-r-mode))
In the course of writing assignments I ran into a problem where certain tidyverse packages were causing weird coloration in the inferior ESS R buffer, such that the text was basically unreadable on a dark background. After some digging it seems that the R process emits super leet haxors ANSI color codes, because you know why not?
The issue is this one: emacs-ess/ESS#1193
And the solution/workaround was:
(defun my-inferior-ess-init ()
"Workaround for https://github.com/emacs-ess/ESS/issues/1193"
(add-hook 'comint-preoutput-filter-functions #'xterm-color-filter -90 t)
(setq-local ansi-color-for-comint-mode nil))
(use-package xterm-color
:config
(add-hook 'inferior-ess-mode-hook #'my-inferior-ess-init))
Most of these are simple invocations of use-package
and require no explanation.
(use-package web-mode)
(use-package glsl-mode
:ensure (:host github :repo "jimhourihan/glsl-mode"))
(use-package python)
(use-package lsp-python-ms
:after (lsp-mode python)
:init (setq lsp-python-ms-auto-install-server t))
(defun lsp-go-install-save-hooks ()
(add-hook 'before-save-hook #'lsp-format-buffer t t)
(add-hook 'before-save-hook #'lsp-organize-imports t t))
(use-package go-mode)
Some generally-useful stuff like Dashboard and packages like Org for writing prose comes here. If you read below and are confused, it’s not a typo: pdflatex
needs to be invoked three times because of the way the standard LaTeX recipe works, which goes something like this:
$ latex <filename>
$ bibtex <filename>
$ latex <filename>
$ latex <filename>
Basically, the first time you run it, latex writes citations and stuff like \label
to a .aux
file, which is what bibtex reads. BibTeX reads that file as well as the .bib
file and uses that to format the references. When you run LaTeX a second time it also reads both .aux
and .tex
files and if bibtex generated any .bbl
files it reads those as well, which is how it inserts the references into the output. The third run is what causes the citations and labels to get inserted into the output. If you have multiple bibliographies you’ll need more invocations of bibtex and latex and it quickly becomes a clusterfuck. Anyways this is why I have three calls to pdflatex
in there.
(use-package dashboard
:config
(add-hook 'elpaca-after-init-hook #'dashboard-insert-startupify-lists)
(add-hook 'elpaca-after-init-hook #'dashboard-initialize)
(add-hook 'window-size-change-functions #'dashboard-resize-on-hook 100)
(add-hook 'window-setup-hook #'dashboard-resize-on-hook)
(setq dashboard-items '((recents . 20) (bookmarks . 20)))
(setq dashboard-banner-logo-title "Hacks and glory await!")
(setq recentf-exclude '("bookmarks"))
(setq dashboard-startup-banner (expand-file-name "dashboard-logo.png" user-emacs-directory)))
(use-package org
:ensure nil
:init
(setf org-list-allow-alphabetical t)
(setf org-src-tab-acts-natively t)
(setf org-startup-truncated nil)
:config
(org-babel-do-load-languages 'org-babel-load-languages '((R . t)
(lisp . t)
(emacs-lisp . t)))
(set-face-attribute 'org-table nil :inherit 'fixed-pitch)
(set-face-attribute 'org-code nil :inherit 'fixed-pitch)
(set-face-attribute 'org-block nil :inherit 'fixed-pitch)
(set-face-attribute 'org-block-begin-line nil :inherit 'fixed-pitch)
(set-face-attribute 'org-block-end-line nil :inherit 'fixed-pitch)
(set-face-attribute 'org-block-begin-line nil :slant 'normal :underline nil :extend nil)
(set-face-attribute 'org-block-end-line nil :slant 'normal :overline nil :extend nil)
(setf org-html-preamble nil)
(setf org-html-postamble nil)
(setq org-latex-listings 'minted)
(setq org-latex-packages-alist '(("" "minted")))
(setq org-latex-pdf-process
'("pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
"pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
"pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f")))
(use-package org-bullets
:init
(add-hook 'org-mode-hook (lambda ()
(org-bullets-mode 1))))
(use-package ox-rfc)
(use-package markdown-mode
:commands (markdown-mode gfm-mode)
:mode (("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init (setq markdown-command "multimarkdown"))
For writing prose or anything non-code I like to use Olivetti which adds some nice gutters on either side of the screen and pair it with variable pitch fonts.
(use-package olivetti
:init
(add-hook 'text-mode-hook (lambda ()
(olivetti-mode 1)
(olivetti-set-width 140)
(variable-pitch-mode 1))))
I put package-local/namespaced binds with their use-package declarations but I decided to collect all my global custom keybinds into one section here at the end to keep better tabs on my C-c <single char>
namespace.
(global-set-key (kbd "C-c d") 'lsp-find-definition)
(global-set-key (kbd "C-c g") 'rgrep)
(global-set-key (kbd "C-c i") 'flip-frame)
(global-set-key (kbd "C-c o") 'flop-frame)
(global-set-key (kbd "C-c r") 'rotate-frame-clockwise)
(global-set-key (kbd "C-c t") 'treemacs)
(global-set-key (kbd "C-c y") 'yas-expand)
(global-set-key (kbd "C-c n") 'parrot-rotate-next-word-at-point)
(global-set-key (kbd "C-c p") 'parrot-rotate-prev-word-at-point)
(global-set-key (kbd "C-c q") 'query-replace)
(global-set-key (kbd "C-c x") 'query-replace-regexp)
(global-set-key (kbd "M-x") 'smex)
(global-set-key (kbd "M-X") 'smex-major-mode-commands)
(global-set-key (kbd "M-o") 'insert-line-below)
;; This is the old M-x.
(global-set-key (kbd "C-c C-c M-x") 'execute-extended-command)
(global-set-key (kbd "C-c +") 'increment-number-at-point)
(global-set-key (kbd "C-c -") 'decrement-number-at-point)
(global-unset-key (kbd "M-t"))