My Emacs Configuration

Table of Contents

  1. General settings
  2. Package Management
  3. Look & Feel
  4. Spell Checking
  5. Multiple Cursors
  6. Command Completion & Search
  7. Help
  8. Markdown
  9. Org Mode
  10. Version Control
  11. Project Navigation
  12. Structural Editing
  13. Clojure
  14. JavaScript
  15. Code Editing
  16. Startup Window

Emacs is a Free programmable text editor that can by highly customized and extended through a rich ecosystem of packages. This configuration performs that customization of Emacs at startup.

General settings

Start by making sure elisp is evaluating the configuration using lexical binding. This makes it easier to reason about what the code does just by looking at its local use, avoiding potential errors.

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

I'm adding a nice warning to remind myself not to edit the config file directly, since I manage this configuration file through Emacs with org-babel and org-babel-tangle.

;; ****************************************************************************
;; *                                                                          *
;; *                WARNING: DO NOT EDIT init.el DIRECTLY!!!                  *
;; *  Edit README.org in Emacs, and let org-babel-tangle generate init.el.    *
;; *                                                                          *
;; ****************************************************************************

Uncomment this line if you need trace information to help troubleshoot, if you run into problems.

;;(setq debug-on-quit t)  

This is a helper function that attempts to load an elisp file, warns if the file cannot be loaded, and (optionally) calls a user-provided function to create default values if the file cannot be loaded.

(defun my/load-or-warn (file warning &optional defaults)
  "Load FILE, or warn and call DEFAULTS if missing or empty."
  (if (and (file-exists-p file) (> (file-attribute-size (file-attributes file)) 0))
      (load file)
    (warn "%s" warning)
    (when defaults (funcall defaults))))

Some packages set custom variables and faces at the end of the Emacs configuration. Since my configuration is generated by org-babel-tangle, I want any automatic customization to happen in a separate file.

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(my/load-or-warn custom-file "custom.el is missing or empty; no customizations loaded.")

The first time you start Emacs, it can output a lot of warnings as packages are downloaded and compiled. I add this to help reduce (not eliminate) as many warnings as I can.

(setq byte-compile-warnings
      '(not
          docstrings
          lexical        ; Covers missing lexical-binding warnings.
          obsolete       ; Suppresses deprecated function warnings.
          cl-functions   ; Suppresses cl / defstruct warnings.
          warnings       ; General noisy category (covers many defcustom issues).
          ))

I have a bad habit of accidentally pressing C-z, which minimizes the Emacs frame. This disables that keyboard combination.

(global-unset-key (kbd "C-z"))

Package Management

So far, every package that I've needed has been available from these two main repositories. I'm not sure why package-initialize is needed (I'm currently running Emacs 30.2), but Emacs fails to load packages without it. eval-when-compile is just an optimization to make startup faster.

(setq package-archives
      '(("gnu"          . "https://elpa.gnu.org/packages/")
        ("melpa"        . "https://melpa.org/packages/")))

(package-initialize)

(eval-when-compile
  (require 'use-package))

Configure use-package to try to install packages automatically, without needing to add :ensure t to every instance of use-package. In cases where you need to use a built-in package, add :ensure nil for just those packages.

(setq use-package-always-ensure t)  

Look & Feel

Default Emacs is ugly. Fortunately we have control over that.

Don't show the splash screen, or any text in the scratch buffer at startup.

(setq inhibit-startup-screen t)
(setq initial-scratch-message nil)  

Use a clean aesthetically-pleasing mode line (status) bar.

(use-package doom-modeline
  :config
  (doom-modeline-mode 1))  

Remove a lot of the clutter that makes the default Emacs UI look busy and outdated. This includes the scroll bar, tool bar, menu bar, and tool tips.

(scroll-bar-mode -1)
(tool-bar-mode -1)
(menu-bar-mode -1)
(tooltip-mode -1)

The "fringe" on Emacs is the small space on the left and right side of the frame. I'll use it later on to show changes to the file compared to the version in the source code repository.

(set-fringe-mode 15)
(set-face-attribute
 'fringe nil
 :background
 (face-background 'default))  

When you try to do something you can't (e.g. page down past the end of the buffer), I like a visual indication of that. Show a brief flash when that happens.

(setq visible-bell t)  

Good fonts make a huge difference. I like the Fira Code Retina font. This will need to be installed on your system. On Linux Mint, that can be done by running sudo apt install fonts-firacode. This configuration will only try to install the font if it is found on your system.

(if (find-font (font-spec :name "Fira Code Retina"))
    (set-face-attribute 'default nil :font "Fira Code Retina" :height 120))  

The Zenburn theme was originally made for Vim users, but it has been ported to many different environments. It's a low-contrast dark theme that makes working for long hours on documents easier. I'm using a couple of tweaks on the theme.

(setq zenburn-use-variable-pitch t)

(setq zenburn-scale-org-headlines t)

(setq zenburn-scale-outline-headlines t)

(use-package zenburn-theme
  :config
  (load-theme 'zenburn t))  

The nerd font icon set is required by doom modeline 4.x and newer. Font icons make a nicer-looking modeline.

(use-package nerd-icons
  :if (display-graphic-p)
    :config
    (unless (find-font (font-spec :name "Symbols Nerd Font Mono"))
      (nerd-icons-install-fonts t)))

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

Line numbers make it easier to keep track of where you are, and to jump to specific lines in the buffer. I like them on by default, and turn them off for some modes.

(global-display-line-numbers-mode t)
(dolist (mode '(org-mode-hook
                org-agenda-mode-hook
                term-mode-hook
                shell-mode-hook
                eshell-mode-hook))
        (add-hook mode (lambda () (display-line-numbers-mode 0))))  

Rainbow delimiters give pairs of parenthesis different colors, making it easier to match an opening paren with its closing paren. This is especially useful for programming Lisp (Clojure, elisp, etc). This package is only enabled for programming modes.

The default colors are a little too subtle, so I change those colors to match the Zenburn theme's full-brightness color palette.

NOTE: If I ever change the theme, I'll need to change these colors to match.

(use-package rainbow-delimiters
  :hook (prog-mode . rainbow-delimiters-mode)
  :custom-face
  (rainbow-delimiters-depth-1-face ((t (:foreground "#F0DFAF"))))  ; yellow
  (rainbow-delimiters-depth-2-face ((t (:foreground "#8CD0D3"))))  ; cyan
  (rainbow-delimiters-depth-3-face ((t (:foreground "#DFAF8F"))))  ; orange
  (rainbow-delimiters-depth-4-face ((t (:foreground "#94BFF3"))))  ; blue
  (rainbow-delimiters-depth-5-face ((t (:foreground "#DC8CC3"))))  ; pink
  (rainbow-delimiters-depth-6-face ((t (:foreground "#8FB28F"))))  ; green
  (rainbow-delimiters-depth-7-face ((t (:foreground "#E0CF9F"))))  ; tan
  (rainbow-delimiters-depth-8-face ((t (:foreground "#7CB8BB"))))  ; teal
  (rainbow-delimiters-depth-9-face ((t (:foreground "#CC9393"))))  ; red
  (rainbow-delimiters-unmatched-face ((t (:foreground "#FF0000" :bold t)))))

Use the dimmer package to make the non-active buffers dimmer, so it's easier to tell which buffer is active at the moment.

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

Add a column indicator (a faint vertical line) to each buffer as a guide to help you avoid writing really long lines. The position of the column indicator can be adjusted for different modes.

(setq-default fill-column 120)
(global-display-fill-column-indicator-mode 1)
(set-face-attribute 'fill-column-indicator nil :foreground "#616161")

(defun my-set-fill-column (n)
  (setq-local fill-column n))

(add-hook 'clojure-mode-hook (lambda () (my-set-fill-column 100)))
(add-hook 'js-mode-hook      (lambda () (my-set-fill-column 100)))
(add-hook 'css-mode-hook     (lambda () (my-set-fill-column 100)))
(add-hook 'html-mode-hook    (lambda () (my-set-fill-column 100)))
(add-hook 'markdown-mode-hook (lambda () (my-set-fill-column 100)))
(add-hook 'org-mode-hook     (lambda () (my-set-fill-column 100)))

Spell Checking

Set up inline spell checking so misspelled words are identified as we type.

(use-package flyspell
  :hook
  ((text-mode . flyspell-mode)
   (prog-mode . flyspell-prog-mode)))

(use-package flyspell-correct
  :after flyspell
  :bind (:map flyspell-mode-map ("C-;" . flyspell-correct-wrapper)))

Multiple Cursors

Multiple cursors will allow editing multiple lines simultaneously.

(use-package multiple-cursors
  :bind (("C-S-c C-S-c" . mc/edit-lines)
         ("C->"         . mc/mark-next-like-this)
         ("C-<"         . mc/mark-previous-like-this)
         ("C-c C-<"     . mc/mark-all-like-this)))

Command Completion & Search

Set up command completion and search so it is much more user-friendly.

;; Vertical completion UI.
(use-package vertico
  :config
  (vertico-mode 1))

;; Matches candidates in any order.
(use-package orderless
  :config
  (setq completion-styles '(orderless)))

;; Provide annotations to describe completions.
(use-package marginalia
  :config
  (marginalia-mode))

;; Command auto complete.
(use-package consult
  :bind (("C-s" . consult-line)
         ("C-x b" . consult-buffer)))

;; Show key bindings following the currently-entered incomplete command.
(use-package which-key
  :ensure nil
  :diminish which-key-mode
  :config
  (which-key-mode)
  (setq which-key-idle-delay 0.5))

Help

Improve help by including more information in the help buffers.

;; Richer help buffers with source links, callers, and type info.
(use-package helpful
  :bind
  (("C-h f" . helpful-callable)
   ("C-h v" . helpful-variable)
   ("C-h k" . helpful-key)
   ("C-h x" . helpful-command)
   ("C-h o" . helpful-symbol)
   ("C-c C-d" . helpful-at-point)))

Markdown

Support editing Markdown-formatted text. This adds syntax highlighting, keyboard shortcuts for common actions (e.g. inserting links), and more.

(use-package markdown-mode
  :mode
  (("\\.md\\'" . markdown-mode)
   ("README\\.md\\'" . gfm-mode))
  :config
  (setq markdown-command "multimarkdown"))

Org Mode

Org mode is the killer app for Emacs. It is used for keeping notes, authoring documents, literate programming (like I'm doing with this configuration file), maintaining to-do lists, planning projects, and more.

Set up the calendar so only the holidays I'm interested in show up in the org agenda. This arguably doesn't belong in the Org Mode section of the configuration, but I added it specifically to clean up the org agenda, so that's why it's here.

(use-package calendar
  :ensure nil
  :config
  (setq holiday-bahai-holidays nil
        holiday-hebrew-holidays nil
        holiday-islamic-holidays nil
        holiday-chinese-holidays nil
        holiday-other-holidays '((holiday-fixed 3 14 "Pi Day")
                                 (holiday-fixed 9 14 "Talk Like a Pirate Day"))))  

A couple of important things with how I use the org package:

  • :ensure nil tells Emacs to use the built-in version instead of trying to get it from a repository.

  • The locations of the org directory and agenda files is in a separate file to make it easier to change, and also makes sharing this configuration easier because there is nothing specific to me in it.

    (use-package org :ensure nil :init ;; The org-locations.el file is where you set user-org-directory and user-org-agenda-files. (setq org-locations-file (expand-file-name "org-locations.el" user-emacs-directory)) (my/load-or-warn org-locations-file "org-locations.el is missing or empty; using default org settings." (lambda () (setq user-org-directory "~/org" user-org-agenda-files '()))) :hook ((org-mode . org-indent-mode) (org-mode . visual-line-mode)) :bind (("C-c l" . org-store-link) ("C-c a" . org-agenda) ("C-c c" . org-capture) :map org-mode-map ("C-c " . org-priority-up) ("C-c " . org-priority-down) ("C-c C-g C-r" . org-shiftmetaright)) :config (add-to-list 'org-modules 'org-habit))

This is how org mode knows which directory and agenda files to use.

(setq org-directory user-org-directory
      org-agenda-files user-org-agenda-files)

Refiling is how you move items between different locations and files in org mode. This customizes a few settings in how that works.

(setq org-refile-targets '((org-agenda-files :maxlevel . 2))
      org-refile-allow-creating-parent-nodes 'confirm
      org-refile-use-outline-path 'file
      org-outline-path-complete-in-steps nil)

Automatically log the time when a todo item is done or canceled in a "drawer", which is just a section of the todo item. This keeps all the activity on a todo item organized.

(setq org-log-done 'time
      org-log-into-drawer t)

Format the org agenda, and tweak some of its behavior.

(setq org-agenda-log-mode-items '(state)
      org-agenda-include-diary t
      org-agenda-start-on-weekday 0
      org-agenda-prefix-format '((agenda . " %i %-12:c%?-12t% s")
                                   (todo   . " %i %-12:c")
                                   (tags   . " %i %-12:c")
                                   (search . " %i %-12:c"))
      org-agenda-skip-scheduled-if-done t
      org-agenda-skip-deadline-if-done t
      org-agenda-skip-scheduled-if-deadline-is-shown t

      org-deadline-warning-days 3)

Habits that you want to improve (e.g. reading, writing, exercise, etc) are added to the agenda. I only want to see them for the current day, and a small graph in the agenda shows how I'm doing in my consistency.

(setq org-habit-show-habits-only-for-today t
      org-habit-graph-column 70)

Emphasis markers are the characters used to make text bold, italic, underlined, etc. I like to hide those when I'm editing an org document.

Pressing RET (the Enter key) while point is on a link should follow the link to the linked document.

I also like to show the UTF-8 characters instead of the text that describes the characters (e.g. α).

(setq org-hide-emphasis-markers t
      org-return-follows-link t
      org-pretty-entities t)

These are some default export headings I like. Add a table of contents to exported documents, remove the "Validate HTML5" link from exported documents, etc.

(setq org-html-validation-link nil
      org-export-with-toc t
      org-export-headline-levels 4
      org-html-htmlize-output-type 'css)

Support Clojure evaluation in org files.

(setq org-babel-clojure-backend 'cider
      org-babel-default-header-args:clojure '((:session . "notebook"))
      org-confirm-babel-evaluate nil
      cider-allow-jack-in-without-project t)

(org-babel-do-load-languages
 'org-babel-load-languages
 '((clojure . t)))

Capture templates make it easy to quickly add items to org files. This is where I define the ones that I need.

(setq org-capture-templates
      `(("b" "Birthday" entry
         (file ,(expand-file-name "birthdays.org" user-org-directory))
         "* %?\n  %^{Birthday}t")

        ("i" "Idea" entry
         (file ,(expand-file-name "ideas.org" user-org-directory))
         ,(concat "* Idea: %^{Title} %^G\n"
                  ":PROPERTIES:\n"
                  ":DATE: %<%Y-%m-%d>\n"
                  ":ABSTRACT: %^{Abstract}\n"
                  ":END:\n\n"
                  "** Description\n\n")
         :empty-lines 1)

        ("n" "Nature Sighting" entry
         (file+headline ,(expand-file-name "nature.org" user-org-directory) "Sightings")
         ,(concat "* Sighting: %^{Species/Event} %^G\n"
                  ":PROPERTIES:\n"
                  ":DATE: %<%Y-%m-%d>\n"
                  ":LOCATION: %^{Location}\n"
                  ":END:\n\n"
                  "** Field Notes\n\n%?")
         :empty-lines 1)

        ("t" "Todo (Inbox)" entry
         (file+headline ,(expand-file-name "todo.org" user-org-directory) "Inbox")
         "* TODO %?\n  %U\n")))

The default bullets when editing in Org Mode are boring. These are nicer.

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

Generate a table of contents by adding :toc: to a heading.

(use-package toc-org
  :after org
  :hook
  (org-mode . toc-org-mode))

When literate programming, automatically generate the code whenever the Org file is saved. This prevents you from having to remember to do it, and also prevents you from building muscle memory from constantly running the tangle after saving.

(use-package org-auto-tangle
  :defer t
  :hook
  (org-mode . org-auto-tangle-mode)
  :config
  (setq org-auto-tangle-default nil))

Make Org look better, especially the org agenda. The current version of org-modern has a bug int it that affects dimmer by making it unable to un-dim the active buffer. I submitted a PR (Pull Request) that fixes the bug, so we'll see if he accepts it. For now I just load the fixed version from my fork of the Github repo.

Another workaround by calling dimmer-restore-buffer is in the commented-out version below.

(use-package org-modern
  :after org
  :ensure nil
  :load-path "/Data/Development/org-modern" ; Use my own fork that fixes the dimmer bug (PR submitted).
  :hook
  ((org-mode . org-modern-mode)
   (org-agenda-finalize . org-modern-agenda))
  :custom
  (org-modern-star nil))

;; (use-package org-modern
;;   :after org
;;   :hook
;;   ((org-mode . org-modern-mode)
;;    (org-agenda-finalize . org-modern-agenda))
;;   :custom
;;   (org-modern-star nil)
;;   :config
;;   ;; org-modern-agenda calls copy-tree on face-remapping-alist, which
;;   ;; invalidates dimmer's face-remap cookies. Restore the buffer first
;;   ;; so the copy captures a clean alist and dimmer can re-evaluate correctly.
;;   (add-hook 'org-agenda-finalize-hook
;; 	      (lambda ()
;;               (when (fboundp 'dimmer-restore-buffer)
;;                 (dimmer-restore-buffer (current-buffer))))))

Version Control

Set up version control.

(use-package magit
  :defer t
    :custom
    (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1))

  (use-package git-modes)

  (use-package diff-hl
    :init
    (global-diff-hl-mode)
    :hook
    (diff-hl-mode . diff-hl-show-hunk-mouse-mode))

Project Navigation

Make it easier to find and switch between projects.

(use-package projectile
  :diminish projectile-mode
  :bind-keymap
  ("C-c p" . projectile-command-map)
  :config
  (projectile-mode)
  (when (file-directory-p "/Data/Development")
    (setq projectile-project-search-path '("/Data/Development")))
  (setq projectile-switch-project-action #'projectile-dired))

Structural Editing

Enable structural editing.

(use-package paredit
  :hook ((clojure-mode    . paredit-mode)
         (emacs-lisp-mode . paredit-mode)
         (cider-repl-mode . paredit-mode)))

Clojure

Support Clojure.

(use-package clojure-mode
  :defer t)

(use-package cider
  :defer t
  :hook (clojure-mode . cider-mode)
  :config
  (setq cider-repl-display-help-banner nil))

(use-package clj-refactor
  :hook (clojure-mode . clj-refactor-mode)
  :config
  (cljr-add-keybindings-with-prefix "C-c r"))

JavaScript

Support JavaScript.

(use-package web-mode
  :mode (("\\.jsx\\'" . web-mode)
         ("\\.tsx\\'" . web-mode))
  :config
  (setq web-mode-markup-indent-offset 2)
  (setq web-mode-css-indent-offset 2)
  (setq web-mode-code-indent-offset 2))

(use-package prettier-js
  :hook ((js-mode   . prettier-js-mode)
         (web-mode  . prettier-js-mode)))

Code Editing

Set up support for enriching source code editing capabilities.

You need to globally install typescript-language-server and typescript npm packages:

$ npm install -g typescript-language-server typescript

You also need to install clojure-lsp:

$ sudo bash < <(curl -s https://raw.githubusercontent.com/clojure-lsp/clojure-lsp/master/install)

(use-package eglot
  :ensure nil
  :hook ((clojure-mode . eglot-ensure)
         (js-mode      . eglot-ensure)
         (web-mode     . eglot-ensure))
  :config
  (add-to-list 'eglot-server-programs
               '(web-mode . ("typescript-language-server" "--stdio"))))

Startup Window

When you launch Emacs, make the org agenda the first thing you see.

(add-hook 'after-init-hook (lambda ()
  (org-agenda-list)
  (delete-other-windows)))