My Emacs Configuration

Add a Makefile rule to setup the configuration on a new machine

Makefile
.PHONY: emacs
emacs:
     test -L $(HOME)/.emacs.d || ln -s $(shell pwd)/emacs/ $(HOME)/.emacs.d

Put all the noise generated by the custom system into a separate file

emacs/init.el
;;; init.el -- Emacs configuration -*- lexical-binding: t -*-
(setq custom-file (locate-user-emacs-file "custom.el"))
(when (file-exists-p custom-file)
 (load custom-file))

Setup package archives

emacs/init.el
(package-initialize)
(add-to-list 'package-archives '("org" . "https://orgmode.org/elpa/"))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))

And install some utility libraries

emacs/init.el
(dolist (pkg '(s))
  (unless (package-installed-p pkg)
    (unless package-archive-contents
      (package-refresh-contents))
    (package-install pkg)))

Work Around

I am currently using Aurora which encourages the use of homebrew to install cli programs. This means executables are normally placed in /home/linuxbrew/.linuxbrew/bin/ which is usually added to PATH for my user account. However, this does not happen when launching Emacs via krunner and so it fails to find any tools installed there.

To work around this, let’s manually add it to exec-path

emacs/init.el
(if (file-exists-p "/home/linuxbrew/.linuxbrew/bin")
    (add-to-list 'exec-path "/home/linuxbrew/.linuxbrew/bin"))

Now that I’ve started running the Emacs server via systemctl --user, $HOME/.local/bin does not appear to be on the PATH either. (No idea why, so let’s just add it here and move on.)

emacs/init.el
(if (file-exists-p "/home/alex/.local/bin")
    (add-to-list 'exec-path "/home/alex/.local/bin"))

However, this isn’t enough to let functions like shell-command find the command, we also need to update Emacs’ version of PATH

emacs/init.el
(setenv "PATH" (string-join exec-path ":"))

Basic Editing

emacs/init.el
(recentf-mode t)
(savehist-mode t)
(save-place-mode t)

Backup Files

By default Emacs will litter folders with backup files (filename.txt~), rather than disable them entirely just put them out of sight.

emacs/init.el
(setq backup-directory-alist `(("." . ,(expand-file-name "backups" user-emacs-directory))))

Repeat Mode

repeat-mode is awesome and while I should really look at trying some of the things shown in Karthink’s guide, simply turning the mode on gives you a lot.

emacs/init.el
(repeat-mode 1)

Whitespace

Visualise certain whitespace characters

face

Enable visualisations that use faces

empty (requires face)

Highlight empty lines at the start/end of the buffer.

trailing (requires face)

Highlight trailing whitespace

lines-char (requires face)

If a line is too long (according to whitespace-line-column), highlight the first character that crosses the threshold

tab-mark

Render tab characters in the buffer (like set list in vim). Uses whitespace-display-mappings to determine the character(s) to use.

emacs/init.el
(setq whitespace-style '(face empty trailing lines-char tab-mark))
(global-whitespace-mode t)

Enable automatic whitespace cleanup on each save

emacs/init.el
(add-hook 'before-save-hook #'whitespace-cleanup)

Indent using spaces by default, and ensure that the TAB key is only used for indentation.

emacs/init.el
(setq tab-always-indent t)
(setq-default indent-tabs-mode nil)

Compilation

Basic settings for Emacs’ compilation framework

emacs/init.el
(setq compilation-always-kill t
      compilation-scroll-output t)

Enable coloured output in compilation buffers

emacs/init.el
(add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)

Attempt at a sane display-buffer-alist rule for compilation windows

emacs/init.el
(add-to-list 'display-buffer-alist
             '("\\*compilation\\*"
               (display-buffer-reuse-window
                display-buffer-in-previous-window
                display-buffer-reuse-mode-window
                display-buffer-at-bottom)
               (window-height . 0.25)))

Make F5 call recompile

emacs/init.el
(keymap-global-set "<f5>" 'recompile)

The following adds a simple minor mode which, when enabled, will call recompile each time the buffer is saved

emacs/init.el
(define-minor-mode recompile-on-save-mode
  "When enabled, run `recompile' after the current buffer is saved"
  :lighter " ⭮"
  (if recompile-on-save-mode
      (add-hook 'after-save-hook 'recompile nil t)
    (remove-hook 'after-save-hook 'recompile t)))

Treesitter

See also

How To Get Started with Tree Sitter

Article on Matering Emacs

Combobulate: Structured Movement and Editing with Tree-Sitter

Another introducing the combobulate package

As of version 29.1 Emacs comes with support out of the box (assuming it has been compiled with the --with-tree-sitter flag)

emacs/init.el
(use-package alc-treesitter
  :load-path "lisp"
  :if (treesit-available-p))

Installing Grammars

While Emacs supports the tree-sitter library itself, it does not come with any grammars which need to be installed separately. Thankfully, Emacs does provide a few utilities to help with this

emacs/lisp/alc-treesitter.el
(setq treesit-language-source-alist
      '((python "https://github.com/tree-sitter/tree-sitter-python" "v0.20.4")
        (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "v0.20.3" "typescript/src")))

The treesit-language-source-alist variable tells Emacs where it can find a given grammar and what language it is for. Once defined (and assuming you have the necessary dependencies) you can use M-x treesit-install-language-grammar command to install one of the grammars you defined. Alternatively this snippet will install them all in one go

(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))

Major Modes

Switching to tree-sitter powered major modes by default would not be backwards compatible, so if we actually want to make use of the tree-sitter support we need to opt into it.

emacs/lisp/alc-treesitter.el
(if (treesit-language-available-p 'python)
  (add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode)))

Combobulate

mickeynp/combobulate provides a nice set of movement and editing commands that leverage the parse tree generated by tree sitter.

emacs/lisp/alc-treesitter.el
 (use-package combobulate
  :vc (:url "https://github.com/mickeynp/combobulate" :rev "master")
  :hook ((python-ts-mode . combobulate-mode)
         (typescript-ts-mode . combobulate-mode)))
emacs/lisp/alc-treesitter.el
(provide 'alc-treesitter)

*Completions*

Use C-n and C-p to cycle through completion candidates.

emacs/init.el
(dolist (kmap (list minibuffer-local-map completion-in-region-mode-map))
  (keymap-set kmap "C-p" #'minibuffer-previous-completion)
  (keymap-set kmap "C-n" #'minibuffer-next-completion))

Only pop open the *Completions* buffer on the second attempt at completing something. However, once it is open keep it updated and only focus it on the second completion attempt with no progress made.

emacs/init.el
(setq completion-auto-help 'visible
      completion-auto-select 'second-tab)

If there are only 3 (or fewer) possible candidates, cycle between them

emacs/init.el
(setq completion-cycle-threshold 3)

Tweak how the *Completions* buffer is rendered

emacs/init.el
(setq completions-detailed t
      completions-format 'one-column
      completions-group t
      completions-header-format (propertize "%s candidates\n" 'face 'shadow)
      completions-max-height 15
      completion-show-help nil)

Use the oantolin/orderless completion style

emacs/init.el
(use-package orderless
  :ensure t
  :custom
  (completion-styles '(orderless basic))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles basic partial-completion)))))

Consult

Use minad/consult

emacs/init.el
(use-package consult
  :ensure t
  :custom
  (consult-preview-key "M-.")
  :bind (("C-x b" . consult-buffer)))

And since I use embark, use the recommended emabrk-consult package

emacs/init.el
(use-package embark-consult
  :after (embark consult)
  :ensure t)

Embark

Use oantolin/embark!

emacs/init.el
(use-package embark
  :ensure t
  :bind (("C-." . embark-act)
         ("M-." . embark-dwim)))

Dired

Settings for dired

emacs/init.el
(use-package dired
 :hook ((dired-mode . dired-hide-details-mode)
        (dired-mode . hl-line-mode))
 :config
 (setq dired-dwim-target t
       dired-maybe-use-globstar t
       dired-isearch-filenames 'dwim
       ;; -A, all files - except '.' & '..'
       ;; -G, omit owning group
       ;; -h, human readable file sizes
       ;; -l, long listing, required for dired.
       dired-listing-switches "-AGhl --group-directories-first --time-style=long-iso"))

project.el

The built-in project management, project.el is good enough for my needs. However, so that Emacs works nicely with monorepo style repositories defining some additional root marker files will get project.el to consider a sub-directory of a repo a valid project.

emacs/init.el
(setq project-vc-extra-root-markers '("Cargo.toml"
                                      "package.json"
                                      "pyproject.toml"))

This however, introduces a few problems the most obvious of which is that project-find-file doesn’t respect .gitignore when run from within a subproject. The following advice ensures that the right vc backend is selected for subprojects

emacs/init.el
(defun alc-project-try-vc-subproject (orig-fun &rest args)
  "Advice for `project-try-vc'.

When using `project-vc-extra-root-markers' to teach project.el
about subprojects within a monorepo, `project-try-vc'
successfully finds the subject's root but fails to detect the
backend. But by calling `vc-responsible-backend' on the found
root, we can fill in the blanks.

As a result, commands like `project-find-file' now respect the
parent repo's .gitignore file even when being run from within a
subproject."
  (let* ((res (apply orig-fun args))
         (dir (nth 2 res))
         (backend (or (nth 1 res)
                      (ignore-errors (vc-responsible-backend dir)))))
    (if dir
        `(vc ,backend ,dir))))

(advice-add 'project-try-vc :around #'alc-project-try-vc-subproject)

Languages

And radian-software/apheleia for automatic formatting of buffers

emacs/init.el
(use-package apheleia
  :ensure t
  :config
  (apheleia-global-mode))

reStructuredText

emacs/init.el
(use-package esbonio
  :vc (:url "https://github.com/swyddfa/esbonio.el" :rev "main")
  :hook ((rst-mode . esbonio-eglot-ensure)))

TypeScript

emacs/init.el
(use-package typescript-ts-mode
  :mode "\\.ts\\'"
  :ensure t
  :hook ((typescript-ts-mode . eglot-ensure)))

YAML

emacs/init.el
(use-package yaml-mode
  :ensure t)

LLMs

emacs/init.el
(use-package gptel
  :ensure t
  :bind (("C-c RET" . gptel-send))
  :config
  (setq gptel-model 'llama3.2:latest)
  (setq gptel-backend (gptel-make-ollama "Ollama"
                       :host "localhost:11434"
                       :stream t
                       :models '(llama3.2:latest))))