Skip to content

SophieBosio/.emacs.d

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sophie’s Emacs Configuration

./images/config-screenshot.png

About

This is my attempt at keeping my Emacs configuration organised and readable.

I write all my initialisation code in this document as code blocks and then use org-babel-tangle to extract those code blocks into a separate file. That new, generated file becomes my init.el. This way, I can document my code and explain my choices to my future self - and to anyone else who might be interested in looking at it. I’ve stolen the code for doing this and several other tidbits from Lars Tveito.

If you’re interested in this approach to writing and sharing your config, it’s called a “literate configuration” and there are lots of great blog posts out there with inspiration and tips!

I’ve lifted a lot of code from other people’s configurations, including:

I can heartily recommend checking those out.

You likely do not want to copy my configuration file, since it’s full of idiosyncrasies and pretty subjective choices. But I do encourage you to take any bits and pieces that seem interesting, try them out, and incorporate the ones you like into your own config.

Table of Contents

Setup

Prerequisites

You probably don’t want to run this configuration as-is, since it’s highly personal and very likely contains things you don’t want in your Emacs.

However, if you do want to try it, or if you want to steal a chunk and something’s not working right, this is the software that I have installed in addition to Emacs and that is present in this config, one way or another.

This doubles as a memo to myself for when I need to set up a new machine.

Here are the programming languages and utils I set up. The configuration for other languages I have in here shouldn’t break anything if you don’t have the accompanying software.

I use these fonts. They are used both in Visuals > Fonts and in Org > Visuals > Fonts.

The rest of what you need should be downloaded by this configuration file. If you try it and find anything missing from this list, please let me know!

init.el Code

As mentioned, I use org-babel-tangle and this document, written in Org mode.

The code below extracts the elisp configuration code and creates/overwrites the ~/.emacs.d/init.el configuration file when the .org-file is saved. Therefore, changes are only done in the .org-file, where writing longer comments about how things work and why things are added is easier, and then the resulting init.el-file remains clean and without excessive comments.

This is what the init.el file should look like, prompting it to tangle the init.org file and replace itself with that code.

;; We need org in order to make use of the tangling functionality
(require 'org)
;; Open the org-mode configuration
(find-file (concat user-emacs-directory "init.org"))
;; Tangle the file
(org-babel-tangle)
;; Load the tangled file
(load-file (concat user-emacs-directory "init.el"))
;; Byte-compile it
(byte-compile-file (concat user-emacs-directory "init.el"))

Git Tracking & Practicalities

Now we also don’t need to track the generated init.el file on Git, since it is directly derived from init.org.

This code makes Git ignore changes to init.el:

git update-index --assume-unchanged init.el

If you do want to start tracking the file again, you can use:

git update-index --no-assume-unchanged init.el

Lexical Scoping

First, I want lexical scoping for the init-file, so I will add that to the top of the file.

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

Tangling

Now to tangling! The rest of the text and code in this section is lifted directly from Lars’ configuration.

The init.el should (after the first run) mirror the source blocks in the init.org. We can use C-c C-v t to run org-babel-tangle, which extracts the code blocks from the current file into a source-specific file (in this case a .el-file).

To avoid doing this each time a change is made we can add a function to the after-save-hook ensuring to always tangle and byte-compile .org-document after changes.

(defun tangle-init ()
  "If the current buffer is init.org the code-blocks are
tangled, and the tangled file is compiled."
  (when (equal (buffer-file-name)
               (expand-file-name (concat user-emacs-directory "init.org")))
    ;; Avoid running hooks when tangling.
    (let ((prog-mode-hook nil))
      (org-babel-tangle)
      (byte-compile-file (concat user-emacs-directory "init.el")))))

(add-hook 'after-save-hook 'tangle-init)

Start-Up

Early Init

Emacs 27 introduced early-init.el, which is like init.el but ran before that, and before the UI and packages are initialised. I’ve taken code snippets from other configs to put in my early-init.el and the blocks in this section tangle to early-init.el instead of init.el.

In particular, the code below is a combination of code from:

;; Defer garbage collection
(setq gc-cons-percentage 0.6)

;; Change default max size for reading processes
(setq read-process-output-max (* 1024 1024)) ;; 1mb

(set-language-environment "UTF-8")

;; Set-language-environment sets default-input-method, which is unwanted.
(setq default-input-method nil)

;; Prefer loading newer compiled files
(setq load-prefer-newer t)

;; Prevent the glimpse of un-styled Emacs by disabling these UI elements early.
(setq default-frame-alist
      '((vertical-scroll-bars . nil)
        (menu-bar-lines       . 0)
        (tool-bar-lines       . 0)))

;; Resizing the Emacs frame can be a terribly expensive part of changing the
;; font. By inhibiting this, we easily halve startup times with fonts that are
;; larger than the system default.
(setq frame-inhibit-implied-resize t
      frame-resize-pixelwise       t)

;; Font compacting can be very resource-intensive, especially when rendering
;; icon fonts on Windows. This will increase memory usage.
(setq inhibit-compacting-font-caches t)

;; Ignore X resources; its settings would be redundant with the other settings
;; in this file and can conflict with later config (particularly where the
;; cursor color is concerned).
(advice-add #'x-apply-session-resources :override #'ignore)

;; A second, case-insensitive pass over `auto-mode-alist' is time wasted.
;; No second pass of case-insensitive search over auto-mode-alist.
(setq auto-mode-case-fold nil)

;; Disable bidirectional text scanning for a modest performance boost.
(setq-default bidi-display-reordering  'left-to-right
              bidi-paragraph-direction 'left-to-right)

;; Unset `file-name-handler-alist' too (temporarily). Every file opened and
;; loaded by Emacs will run through this list to check for a proper handler for
;; the file, but during startup, it won’t need any of them.
(defvar file-name-handler-alist-old file-name-handler-alist)
(setq file-name-handler-alist nil)
(add-hook 'emacs-startup-hook
          (lambda ()
            (setq file-name-handler-alist file-name-handler-alist-old)))

;; For LSP mode, use plists for deserialization
;; For more info, see https://emacs-lsp.github.io/lsp-mode/page/performance/#use-plists-for-deserialization
(setenv "LSP_USE_PLISTS" "true")

;; Remove "For information about GNU Emacs..." message at startup
(advice-add #'display-startup-echo-area-message :override #'ignore)

;; Suppress the vanilla startup screen completely. Even if disabled with
;; `inhibit-startup-screen', it would still initialize anyway.
(advice-add #'display-startup-screen :override #'ignore)

;; Shave seconds off startup time by starting the scratch buffer in
;; `fundamental-mode'
(setq initial-major-mode 'fundamental-mode
      initial-scratch-message nil)

;; Disable startup screens and messages
(setq inhibit-splash-screen t)

Garbage Collection

Famously, the Emacs garbage collector can impede startup times quite dramatically. Therefore, a common tweak is to disable the garbage collector during initialisation, and then resetting it afterwards. Luckily, there exists a package exactly for this purpose called the Garbage Collector Magic Hack-

(use-package gcmh
  :config
  (setq gcmh-idle-delay 5
        gcmh-high-cons-threshold (* 100 1024 1024))  ; 100mb
  (gcmh-mode 1))

Optimisations

We can set the file-name-handler-alist, which is supposed to help startup times a little.

(setq file-name-handler-alist-original file-name-handler-alist)
(setq file-name-handler-alist nil)

I also get quite a lot of compilation warnings, especially from native compilation, but they are usually safe to ignore.

(setq native-comp-async-report-warnings-errors 'silent) ;; native-comp warning
(setq byte-compile-warnings '(not free-vars unresolved noruntime lexical make-local))

Disable warnings about obsolete functions when compiling.

(eval-when-compile
  (dolist (sym '(cl-flet lisp-complete-symbol))
    (setplist sym (use-package-plist-delete
                   (symbol-plist sym) 'byte-obsolete-info))))

This is an optimisation borrowed from Doom Emacs’ core.el.

(setq which-func-update-delay 1.0)

Fix IO bugs.

(setq process-adaptive-read-buffering nil)
(setq read-process-output-max (* 4 1024 1024))

Prevent Emacs from freezing when updating ELPA.

(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")

House-Keeping

Then I want to do some house keeping. First, let’s set the Emacs user and default directories explicitly:

(setq user-emacs-directory "~/.emacs.d/")
(setq default-directory "~/")

Set UFT-8 as preferred coding system.

(set-language-environment    "UTF-8")
(setq locale-coding-system   'utf-8)
(prefer-coding-system        'utf-8)
(set-default-coding-systems  'utf-8)
(set-terminal-coding-system  'utf-8)
(set-keyboard-coding-system  'utf-8)
(set-terminal-coding-system  'utf-8)
(set-keyboard-coding-system  'utf-8)
(set-selection-coding-system 'utf-8)

Don’t warn me when opening files unless over 50 MB.

(setq large-file-warning-threshold (* 50 1024 1024))

Package Manager & Package Sources

To manage downloaded packages, Emacs comes with package.el installed. In addition, I want to use use-package, so let’s make sure we have those loaded.

(require 'package)
(require 'use-package)
(require 'use-package-ensure)
(setq use-package-always-ensure t)

Next, I’ll set up my package sources. These are very common and well-maintained mirrors.

(setq package-archives
      '(("GNU ELPA"     . "https://elpa.gnu.org/packages/")
        ("MELPA"        . "https://melpa.org/packages/")
        ("ORG"          . "https://orgmode.org/elpa/")
        ("MELPA Stable" . "https://stable.melpa.org/packages/")
        ("nongnu"       . "https://elpa.nongnu.org/nongnu/"))
      package-archive-priorities
      '(("GNU ELPA"     . 20)
        ("MELPA"        . 15)
        ("ORG"          . 10)
        ("MELPA Stable" . 5)
        ("nongnu"       . 0)))
(package-initialize)

Local Files

I have a folder with extensions that have been downloaded manually. I’ll add these to the load-path so Emacs knows where to look for them. My folder is called “local-lisp”.

(defvar local-lisp (concat user-emacs-directory "local-lisp/"))
(add-to-list 'load-path  local-lisp)
(let ((default-directory local-lisp))
  (normal-top-level-add-subdirs-to-load-path))

I’ll initialise the list of Org agenda files to an empty list. There are used for task management and for my calendar, and I’ll add to the list both in private.el and in the Org section Tasks.

(setq org-agenda-files '())

And load custom settings from custom.el and private settings from private.el if they exist.

(add-hook
 'after-init-hook
 (lambda ()
   (let ((private-file (concat user-emacs-directory "private.el"))
		 (custom-file (concat user-emacs-directory "custom.el")))
     (when (file-exists-p private-file)
       (load-file private-file))
     (when custom-file
       (load-file custom-file))
     (server-start))))

Terminal Setup

Track current directory in shell.

(dirtrack-mode t)

Mac OS Environment Variables

On Mac, the environment variables aren’t synchronised automatically between the shell and Emacs. exec-path-from-shell fixes that.

(use-package exec-path-from-shell
  :if (memq window-system '(mac ns))
  :config
  (exec-path-from-shell-initialize))

On Mac, I ran into some trouble with my shell, so I specify the shell as a safeguard against random errors.

(when (eq system-type 'darwin)
  (setq vterm-shell "/opt/homebrew/bin/fish"))

DWIM Shell Commands

DWIM Shell Commands (“Do What I Mean” shell commands) are a collection of command-line utilities integrated with Emacs. We’ll load the optional package with pre-configured commands as well.

(use-package dwim-shell-command
  :defer t
  :init (require 'dwim-shell-commands))

Custom Keybindings

Custom Keymap

I keep a custom keybinding map that I add to per package, and then activate at the end of the configuration. This keeps my custom bindings from being overwritten by extensions’ own bindings.

The first step is to create the custom keybinding map. We’ll add bindings to it throughout the config, and then activate it at the end of the config file, at Activating Custom Keybindings.

(defvar custom-bindings-map (make-keymap)
  "A keymap for custom keybindings.")

Mac OS Modifier Keys

On a Mac, I would want to add some specific settings. As a note to myself, I have the following settings in Mac OS:

caps-lock -> control (ctrl)
control   -> control (ctrl)
option    -> option  (alt)
command   -> command (meta)
(setq mac-command-modifier       'meta
      mac-right-command-modifier 'meta
      mac-option-modifier        nil
      mac-right-option-modifier  nil)

Unbind Some Default Keys

Some of the default keybindings are annoying, so let’s unbind them.

I never mean to press C-x C-z, which hides the current Emacs frame.

I also don’t like using C-<wheel up/down> to zoom, which I often do accidentally.

(global-unset-key (kbd "C-x C-z"))
(global-unset-key (kbd "C-<wheel-up>"))
(global-unset-key (kbd "C-<wheel-down>"))

Visuals

Decluttering

Let’s declutter a little. This should have gone into early-init.el, but I get strange compilation warnings (optimiser says there’s too much on the stack).

(dolist (mode
         '(tool-bar-mode       ;; Remove toolbar
           scroll-bar-mode     ;; Remove scollbars
           menu-bar-mode       ;; Remove menu bar
           blink-cursor-mode)) ;; Solid cursor, not blinking
  (funcall mode 0))

This wouldn’t go into early-init anyways.

(setq inhibit-startup-message           t       ;; No startup message
      inhibit-startup-echo-area-message t       ;; No startup message in echo area
      inhibit-startup-screen            t       ;; No default startup screen
      initial-scratch-message           nil     ;; Empty scratch buffer
      initial-buffer-choice             t       ;; *scratch* is default startup buffer
      initial-major-mode                'fundamental-mode
      ring-bell-function                'ignore ;; No bell
      display-time-default-load-average nil     ;; Don't show me load time
      scroll-margin                     0       ;; Space between top/bottom
      use-dialog-box                    nil)    ;; Disable dialog

Frames & Windows

Open in Fullscreen

When I open Emacs, I want it to open maximised and fullscreen by default.

(add-to-list 'default-frame-alist     '(fullscreen . maximized))
;; (add-hook 'window-setup-hook          'toggle-frame-fullscreen t)  ;; F11

Frame Transparency

This doesn’t work ideally, but it does the job. I use it very rarely.

(defun toggle-transparency ()
  (interactive)
  (let ((alpha (frame-parameter nil 'alpha)))
    (set-frame-parameter
     nil 'alpha
     (if (eql (cond ((numberp alpha) alpha)
                    ((numberp (cdr alpha)) (cdr alpha))
                    ;; Also handle undocumented (<active> <inactive>) form.
                    ((numberp (cadr alpha)) (cadr alpha)))
              100)
         '(90 . 55) '(100 . 100)))))
(global-set-key (kbd "C-c h t") 'toggle-transparency)

Frame Border

I want a small border around the whole frame, because I think it looks nicer.

(add-to-list 'default-frame-alist '(internal-border-width . 16))

Some settings to fringes.

(set-fringe-mode 10)                          ;; Set fringe width to 10

(setq-default fringes-outside-margins nil)
(setq-default indicate-buffer-boundaries nil) ;; Otherwise shows a corner icon on the edge
(setq-default indicate-empty-lines nil)       ;; Otherwise there are weird fringes on blank lines

(set-face-attribute 'header-line t :inherit 'default)

Title Bar on Mac OS

I use Emacs Plus port for Mac OS. With it, you can get a transparent title bar (i.e., title bar is same colour as theme background) which I think is really nice.

First, install Emacs Plus.

# enable tap
brew tap d12frosted/emacs-plus

# install
brew install emacs-plus [options]

Then add the corresponding settings to your init-file.

There are two different styles you can choose from. You can have absolutely no title bar on your window or you can have a transparent bar, which still has the three stoplight buttons in the upper-left corner.

For natural title bar, use ns-transparent-titlebar and for no title bar, use undecorated or undercorated-round.

I also set some other options. For example, I don’t need info in the title bar about which buffer is in focus, since this info is already in the mode line. I found these options in this blog post.

(when (eq system-type 'darwin)
  ; no title bar
  (add-to-list 'default-frame-alist '(undecorated-round . t))
  ; don't use proxy icon
  (setq ns-use-proxy-icon nil)
  ; don't show buffer name in title bar
  (setq frame-title-format ""))

Finally, in your terminal, run these commands to use transparent title bar and to hide the icon from the middle of the title bar. I found these in the aforementioned blog post and in the Emacs-Mac Port’s wiki page on the subject.

# for dark themes
defaults write org.gnu.Emacs TransparentTitleBar DARK

# for light themes
defaults write org.gnu.Emacs TransparentTitleBar LIGHT

# hide document icon from title bar
defaults write org.gnu.Emacs HideDocumentIcon YES

Plain Title Bar on GNOME

On GNOME, I can’t get a transparent/native title bar. But I can remove the text from the middle, so it’s completely plain.

(when (eq system-type 'gnu/linux)
  ; don't show buffer name in title bar
  (setq frame-title-format nil))

Programming-Specific Visuals

Cursor

I prefer a bar cursor over a block cursor.

(setq-default cursor-type 'bar)

Having a thin cursor can make it hard to see where you are after switching buffers or jumping around. Beacon highlights your cursor temporarily, which immediately answers the question “Woah, where am I now?”

(use-package beacon
  :defer t
  :init  (beacon-mode 1)
  :bind (:map custom-bindings-map ("C-:" . beacon-blink))
  :config
  (setq beacon-blink-when-window-scrolls nil))

Styling Delimiters

When coding, I want my delimiters (parentheses, brackets, etc.) to be colourised in pairs. rainbow-delimiters does exactly that.

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

Also, please highlight matching parentheses/delimiters.

(show-paren-mode t) ;; Highlight matching parentheses

Line Numbers

I usually only need line numbers in programming mode.

(add-hook 'prog-mode-hook 'display-line-numbers-mode)

Uniquify Buffers

When opening the files foo/bar/name and baz/bar/name, use forward slashes to distinguish them. Default behaviour is angle brackets, which would yield name<foo/bar> and name<baz/bar>..

(require 'uniquify)
(setq uniquify-buffer-name-style 'forward)

Highlight Long Lines

Highlight lines over 120 characters long.

(setq my-whitespace-style '(face tabs lines-tail)
      whitespace-style my-whitespace-style
      whitespace-line-column 120
      fill-column 120
      whitespace-display-mappings
      '((space-mark 32 [183] [46])
        (newline-mark 10 [36 10])
        (tab-mark 9 [9655 9] [92 9])))

;; in e.g. clojure-mode-hook
;; (whitespace-mode 1)
;; or globally
;; (global-whitespace-mode 1)
(add-hook 'prog-mode 'whitespace-mode)

Fonts

Default, Fixed, and Variable Fonts

Please note that I scale and set Org-specific faces in the Org > Visuals section.

For the fixed-pitch font, I’m using the excellent Fragment Mono, which has great ligature support.

I have Open Sans configured as my variable-pitch font.

(defvar soph/font-height 102)

(when (eq system-type 'darwin)
  (setq soph/font-height 130))

(when (member "Fragment Mono" (font-family-list))
  (set-face-attribute 'default nil :font "Fragment Mono" :height soph/font-height)
  (set-face-attribute 'fixed-pitch nil :family "Fragment Mono"))

(when (member "Open Sans" (font-family-list))
  (set-face-attribute 'variable-pitch nil :family "Open Sans"))

Mixed Pitch Fonts

mixed-pitch allows you to mix fixed and variable pitched faces in Org and LaTeX mode.

(use-package mixed-pitch
  :defer t
  :hook ((org-mode   . mixed-pitch-mode)
         (LaTeX-mode . mixed-pitch-mode)))

Ligatures

The package ligature.el provides support for displaying the ligatures of fonts that already have ligatures. Mine does, and seems to work just fine out of the box with the ligatures defined on the package’s page,

(defvar ligature-def '("|||>" "<|||" "<==>" "<!--" "####" "~~>" "***" "||=" "||>"
                       ":::" "::=" "=:=" "===" "==>" "=!=" "=>>" "=<<" "=/=" "!=="
                       "!!." ">=>" ">>=" ">>>" ">>-" ">->" "->>" "-->" "---" "-<<"
                       "<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->"
                       "<--" "<-<" "<<=" "<<-" "<<<" "<+>" "</>" "###" "#_(" "..<"
                       "..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~="
                       "~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
                       "[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
                       ">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
                       "<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!"
                       "##" "#(" "#?" "#_" "%%" ".=" ".-" ".." ".?" "+>" "++" "?:"
                       "?=" "?." "??" ";;" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)"
                       "\\\\" "://"))

(use-package ligature
  :config
  (ligature-set-ligatures 'prog-mode ligature-def)
  (global-ligature-mode t))

Zoom

The default zoom step is a little much on my Linux (Gnome 46) laptop, so let’s decrease it a little from its default value of 1.2.

(setq text-scale-mode-step 1.1)

Beyond that, I often want to scale all the text in the UI when I change text size. Purcell’s default-text-scale does that, so I’ll rebind the standard C-x C-+, C-x C-- and C-x C-0 to the default-text-scale functions.

(use-package default-text-scale
  :defer t
  :bind (:map custom-bindings-map
              ("C-x C-+" . default-text-scale-increase)
              ("C-x C--" . default-text-scale-decrease)
              ("C-x C-0" . default-text-scale-reset)))

Icons & Emojis

Add nerd-icons.

(use-package nerd-icons)

I also want to be able to display emojis with the Apple emoji font. I usually don’t use it, though, so I won’t activate the global mode.

(use-package emojify
  :config
  (when (member "Apple Color Emoji" (font-family-list))
    (set-fontset-font
      t 'symbol (font-spec :family "Apple Color Emoji") nil 'prepend)))

Themes

I really like the doom-themes package, in particular their port of the Nord theme.

(use-package doom-themes
  :config
  (setq doom-themes-enable-bold t     ; if nil, bold is universally disabled
        doom-themes-enable-italic t)) ; if nil, italics is universally disabled

I also have a custom light theme I’m working on called South. Let’s add the path to that so I can load it.

(setq custom-theme-directory "~/Dropbox/projects/south-theme/")

And I don’t want Emacs to ask me before changing to one of the themes I’ve used before.

(setq custom-safe-themes t)

Default Dark & Light Themes

My favourite dark theme is doom-nord. I haven’t been able to find any light themes I really love, so I made South to act as Nord’s bright counterpart. I’ll set these two as my default dark and light themes respectively, and load the dark theme by default.

I’ll also define a default accent colour, which is used in packages like eval-sexp-fu, or wherever I need to define a popping colour outside the theme itself.

(defvar soph/default-dark-theme  'doom-nord)
(defvar soph/default-light-theme 'south)

(defvar soph/default-dark-accent-colour  "SkyBlue4")
(defvar soph/default-light-accent-colour "#CEE4F5")

(load-theme soph/default-dark-theme t)

Changing Theme With System Theme

auto-dark-emacs is a package for switching themes with the system theme. It works both on Linux and on MacOS.

In the hook, I’ll set the colour of the eval-sexp-fu flash to the default-{dark/light}-accent-colour.

For some reason, my light themes leave some fragments that disappear when I load the theme twice, so I’ll do that too.

(use-package autothemer
  :defer t)

(use-package auto-dark
  :ensure t
  :hook ((auto-dark-dark-mode
          .
          (lambda ()
            (interactive)
            (progn
              (custom-set-faces
               `(eval-sexp-fu-flash
                 ((t (:background
                      ,soph/default-dark-accent-colour)))))
              `(load-theme ,soph/default-dark-theme t))))
         (auto-dark-light-mode
          .
          (lambda ()
            (interactive)
            (progn
              (custom-set-faces
               `(eval-sexp-fu-flash
                 ((t (:background
                      ,soph/default-light-accent-colour)))))
              `(load-theme ,soph/default-light-theme t)))))
  :custom
  (auto-dark-themes                   `((,soph/default-dark-theme) (,soph/default-light-theme)))
  (auto-dark-polling-interval-seconds 5)
  (auto-dark-allow-osascript          t)
  :init (auto-dark-mode t))

We can even change the system theme from within Emacs using a dwim-shell-command for Mac OS. The Gnome extension Night Theme Switcher takes care of things on my Linux machine.

(when (eq system-type 'darwin)
  (define-key custom-bindings-map (kbd "M-T") 'dwim-shell-commands-macos-toggle-dark-mode))

Conflict-Free Theme Changing

When changing themes interactively, as with M-x load-theme, the current custom theme is not disabled and this causes some weird issues. For example, the borders around posframes disappear. This snippet from Lars’ config advises load-theme to always disable the currently enabled themes before switching.

(defun disable-custom-themes (theme &optional no-confirm no-enable)
  (mapc 'disable-theme custom-enabled-themes))

(advice-add 'load-theme :before #'disable-custom-themes)

Mode Line

Column Number

Show current column number in mode line.

(column-number-mode t) ;; Show current column number in mode line

Custom Mode Line

Customising the default mode line is thankfully pretty easy. Note that I use the nerd-icons package for the VC branch symbol in the code below. I’ve also borrowed some code from this blog post by Amit Patel on writing a custom mode line.

This mode line is heavily inspired by Nicolas Rougier’s Nano Modeline and he even helped me figure out how to add vertical padding to it.

It has this shape: [ lambda <filename> <git branch name> <LSP code actions> LLLL:CCCC ]

Here’s a screenshot of a small window where the mode line shows well. The number and a little star icon in the bottom right to tell me how many LSP code actions are available at point.

./images/mode-line-screenshot.png

(defvar lsp-modeline--code-actions-string nil)

(setq-default mode-line-format
  '("%e"
	(:propertize " " display (raise +0.4)) ;; Top padding
	(:propertize " " display (raise -0.4)) ;; Bottom padding

	(:propertize "λ " face font-lock-comment-face)
	mode-line-frame-identification
	mode-line-buffer-identification

	;; Version control info
	(:eval (when-let (vc vc-mode)
			 ;; Use a pretty branch symbol in front of the branch name
			 (list (propertize "" 'face 'font-lock-comment-face)
                   ;; Truncate branch name to 50 characters
				   (propertize (truncate-string-to-width
                                (substring vc 5) 50)
							   'face 'font-lock-comment-face))))

	;; Add space to align to the right
	(:eval (propertize
			 " " 'display
			 `((space :align-to
					  (-  (+ right right-fringe right-margin)
						 ,(+ 3
                             (string-width (or lsp-modeline--code-actions-string ""))
                             (string-width "%4l:3%c")))))))

    ;; LSP code actions
    (:eval (or lsp-modeline--code-actions-string ""))
	
	;; Line and column numbers
	(:propertize "%4l:%c" face mode-line-buffer-id)))

Hide Mode Line

hide-mode-line-mode is extracted from Doom Emacs, and does what it says on the tin. It can also be added to hooks to hide the mode line in certain modes. I have it bound to C-c h m - mneumonically “User command: Hide Modeline”.

(use-package hide-mode-line
  :defer t
  :bind (:map custom-bindings-map ("C-c h m" . hide-mode-line-mode)))

Text Display Modes

Olivetti

Olivetti is a minor mode for centering text. For convenience, I’ll bind it to C-c o to activate/deactivate it on the fly.

(use-package olivetti
  :defer t
  :bind (:map custom-bindings-map ("C-c o" . olivetti-mode))
  :config
  (setq olivetti-style t))

Adaptive Wrap

In addition, I use adaptive-wrap to visually wrap lines.

(use-package adaptive-wrap
  :defer t
  :hook (visual-line-mode . adaptive-wrap-prefix-mode))

Writeroom Mode

Writeroom Mode gives you a distraction-free writing environment.

(use-package writeroom-mode
  :defer t)

Focus

Focus dims surrounding text in a semantic manner (sentences, paragraphs, sections, code blocks, etc.) making it easier to, well, focus. I find this especially helpful when editing LaTeX.

(use-package focus
  :defer t)

Presentation Mode

For presenting (e.g., code or Org mode buffers), it’s useful to increase the font size, without necessarily increasing the size of everything else.

(use-package presentation
  :defer t
  :config
  (setq presentation-default-text-scale 2.5))

General Editing

Built-In Options

(delete-selection-mode   t) ;; Replace selected text when yanking
(global-so-long-mode     t) ;; Mitigate performance for long lines
(global-visual-line-mode t) ;; Break lines instead of truncating them
(global-auto-revert-mode t) ;; Revert buffers automatically when they change
(recentf-mode            t) ;; Remember recently opened files
(savehist-mode           t) ;; Remember minibuffer prompt history
(save-place-mode         t) ;; Remember last cursor location in file
(setq auto-revert-interval         1         ;; Refresh buffers fast
      auto-revert-verbose          nil       ;; Don't notify me about reverts
      echo-keystrokes              0.1       ;; Show keystrokes fast
      frame-inhibit-implied-resize 1         ;; Don't resize frame implicitly
      sentence-end-double-space    nil       ;; No double spaces
      recentf-max-saved-items      1000      ;; Show more recent files
      use-short-answers            t         ;; 'y'/'n' instead of 'yes'/'no' etc.
      save-interprogram-paste-before-kill t  ;; Save copies between programs
      history-length               25        ;; Only save the last 25 minibuffer prompts
      global-auto-revert-non-file-buffers t) ;; Revert Dired and other buffers
(setq-default tab-width              4  ;; Smaller tabs
              frame-resize-pixelwise t) ;; Fine-grained frame resize

Smoother Scrolling

I want scrolling to be a lot slower than it is by default.

(setq scroll-conservatively            101
      mouse-wheel-follow-mouse         't
      mouse-wheel-progressive-speed    nil
      ;; Scroll 1 line at a time, instead of default 5
      ;; Hold shift to scroll faster and meta to scroll very fast
      mouse-wheel-scroll-amount        '(1 ((shift) . 3) ((meta) . 6)))

;; (Native) smooooooth scrolling
(setq pixel-scroll-precision-mode t)

(setq mac-redisplay-dont-reset-vscroll t
      mac-mouse-wheel-smooth-scroll    nil)

Tabs & Indentation

One of the things that drove me the most insane when I first downloaded Emacs, was the way it deals with indentation.

I want to use spaces instead of tabs. But if I’m working on a project that does use tabs, I don’t want to mess with other people’s code, so I’ve used this snippet from the Emacs Wiki to infer indentation style.

(defun infer-indentation-style ()
  "Default to no tabs, but use tabs if already in project"
  (let ((space-count (how-many "^  " (point-min) (point-max)))
        (tab-count   (how-many "^\t" (point-min) (point-max))))
    (if (> space-count tab-count) (setq-default indent-tabs-mode nil))
    (if (> tab-count space-count) (setq-default indent-tabs-mode t))))

(setq-default indent-tabs-mode nil)
(infer-indentation-style)

Set backtab to indent-rigidly-left. Then I can easily unindent regions that use spaces instead of tabs.

(define-key custom-bindings-map (kbd "<backtab>") 'indent-rigidly-left)

And finally, make backspace remove the whole tab instead of just deleting one space.

(setq backward-delete-char-untabify-method 'hungry)

Deleting Instead of Killing

Another thing that bothered me, was how the backward-kill-word command (C-delete/backspace) would delete not only trailing backspaces, but everything behind it until it had deleted a word. Additionally, this was automatically added to the kill ring. With this the help of some regexps, it behaves more like normal Ctrl-Backspace.

The code is taken from this and this Stack Exchange/Overflow post.

(defun soph/delete-dont-kill (arg)
  "Delete characters backward until encountering the beginning of a word.
   With argument ARG, do this that many times. Don't add to kill ring."
  (interactive "p")
  (delete-region (point) (progn (backward-word arg) (point))))

(defun soph/backward-delete ()
  "Delete a word, a character, or whitespace."
  (interactive)
  (cond
   ;; If you see a word, delete all of it
   ((looking-back (rx (char word)) 1)
    (soph/delete-dont-kill 1))
   ;; If you see a single whitespace and a word, delete both together
   ((looking-back (rx (seq (char word) (= 1 blank))) 1)
	(soph/delete-dont-kill 1))
   ;; If you see several whitespaces, delete them until the next word
   ((looking-back (rx (char blank)) 1)
    (delete-horizontal-space t))
   ;; If you see a single non-word character, delete that
   (t
    (backward-delete-char-untabify 1))))

Let’s bind this in my custom keybindings map.

(define-key custom-bindings-map [C-backspace] 'soph/backward-delete)

Browse Kill Ring

Speaking of killing text, it’s nice to be able to browse the kill ring.

(use-package browse-kill-ring
  :defer t)

Auto-Saving

To avoid clutter, let’s put all the auto-saved files into one and the same directory.

(defvar emacs-autosave-directory
  (concat user-emacs-directory "autosaves/")
  "This variable dictates where to put auto saves. It is set to a
  directory called autosaves located wherever your .emacs.d/ is
  located.")

;; Sets all files to be backed up and auto saved in a single directory.
(setq backup-directory-alist
      `((".*" . ,emacs-autosave-directory))
      auto-save-file-name-transforms
      `((".*" ,emacs-autosave-directory t)))

I prefer having my files save automatically. Any changes I don’t want, I just don’t commit to git. I use auto-save-buffers-enhanced to automatically save all buffers, not just the ones I have open.

But since saving this file - the init.org-file - triggers recompilation of init.el, it’s really annoying if this file is autosaved when I write to it. Therefore, I’ll disable automatic saving for this file in particular.

(use-package auto-save-buffers-enhanced
  :ensure t
  :config
  (auto-save-buffers-enhanced t)
  (setq auto-save-buffers-enhanced-exclude-regexps '("init.org")))

Move Where I Mean

mwim (Move Where I Mean) takes semantics and indentation into account. This lets us rebind C-a and C-e to move to the beginning and end of a line while respecting indentation. I.e., don’t move to the actual beginning of the line, but to indentation.

(use-package mwim
  :ensure t
  :bind (:map custom-bindings-map
              ("C-a" . mwim-beginning-of-code-or-line)
              ("C-e" . mwim-end-of-code-or-line)))

Text Editing Functions

Expand Region

expand-region expand the region (selected text) with semantic units (e.g., symbol, word, sentence, paragraph). It’s super handy!

M-q is bound to fill-paragraph. I don’t use that binding, but you might want to bind this to a different key combo if you do.

(use-package expand-region
  :defer t
  :bind (:map custom-bindings-map
              ("M-q" . er/expand-region)))

Filling/Unfilling

In Emacs, paragraphs can be padded by a bunch of newlines, meaning a what looks like a normal paragraph in Emacs (one line) is actually several lines with \n all over. This function removes those and makes the selected region one line again.

;;; Stefan Monnier <foo at acm.org>. It is the opposite of fill-paragraph
(defun unfill-paragraph (&optional region)
  "Takes a multi-line paragraph and makes it into a single line of text."
  (interactive (progn (barf-if-buffer-read-only) '(t)))
  (let ((fill-column (point-max))
		;; This would override `fill-column' if it's an integer.
		(emacs-lisp-docstring-fill-column t))
	(fill-paragraph nil region)))
;; Handy key definition
(define-key custom-bindings-map (kbd "C-c n q") 'unfill-paragraph)

Multiple Cursors & Symbol Overlay

multiple-cursors makes life so much easier! I often use it to create several cursors directly above one another. I’ll trust myself to wield this power responsibly and set the variable mc/always-run-for-all to t, which disables the default behaviour prompting the user for confirmation when trying to do certain things with the multiple cursors.

(use-package multiple-cursors
  :defer t
  :functions
  mc/remove-fake-cursors
  mc/save-excursion
  mc/create-fake-cursor-at-point
  mc/maybe-multiple-cursors-mode
  :bind (:map custom-bindings-map
              ("M-n" . mc/mark-next-like-this)
              ("M-p" . mc/mark-previous-like-this))
  :config
  (setq mc/always-run-for-all t))

symbol-overlay highlights all occurrences of the symbol at point and allows to jump between them.

(use-package symbol-overlay
  :defer t
  :functions
  symbol-overlay-put
  symbol-overlay-mode
  :hook (prog-mode . symbol-overlay-mode)
  :bind (:map custom-bindings-map
              ("M-M" . symbol-overlay-put)
              ("M-N" . symbol-overlay-jump-next)
              ("M-P" . symbol-overlay-jump-previous)))

In his blog post, Alvaro Ramirez (AKA Xenodium) demonstrates one of the best things in Emacs: Seeing things that are almost the way you want them and tweaking them with Elisp so they become that. He takes multiple-cursors and symbol-overlay and combines them. and Ramirez wrote a function that lets symbol-overlay communicate to multiple-cursors that this is where you should give me cursors. Edit all the things at once! I think it’s great, so let’s use it and bind it to C-;.

(defun ar/mc-mark-all-symbol-overlays ()
  "Mark all symbol overlays using multiple cursors."
  (interactive)
  (mc/remove-fake-cursors)
  (when-let* ((overlays (symbol-overlay-get-list 0))
              (point (point))
              (point-overlay (seq-find
                              (lambda (overlay)
                                (and (<= (overlay-start overlay) point)
                                     (<= point (overlay-end overlay))))
                              overlays))
              (offset (- point (overlay-start point-overlay))))
    (setq deactivate-mark t)
    (mapc (lambda (overlay)
            (unless (eq overlay point-overlay)
              (mc/save-excursion
               (goto-char (+ (overlay-start overlay) offset))
               (mc/create-fake-cursor-at-point))))
          overlays)
    (mc/maybe-multiple-cursors-mode)))

(define-key custom-bindings-map (kbd "C-;") 'ar/mc-mark-all-symbol-overlays)

Undo/Redo

The default “undo until you can redo” behaviour of Emacs still trips me up. undo-fu lets me specify keys to “only undo” or “only redo”.

(use-package undo-fu
  :defer t
  :bind (:map custom-bindings-map
              ("C-_" . undo-fu-only-undo)
              ("M-_" . undo-fu-only-redo)))

Move Lines

move-dup provides bindings for moving and duplicating whole lines. It’s super convenient.

(use-package move-dup
  :bind (:map custom-bindings-map
              (("C-M-<up>"   . move-dup-move-lines-up)
               ("C-M-<down>" . move-dup-move-lines-down))))

Join Lines

From What the .emacs.d!?, a keybinding to join the line below with the one above.

(define-key custom-bindings-map
            (kbd "M-j")
            (lambda ()
              (interactive)
              (join-line -1)))

Kill Whole Line

(define-key custom-bindings-map (kbd "C-S-k") 'kill-whole-line)

CRUX

CRUX is a Collection of Ridiculously Useful eXtensions for Emacs. It has a whole bunch of commands and I’d recommend looking into all the things it supports.

(use-package crux
  :defer t
  :bind (:map custom-bindings-map
         ("C-S-<return>" . crux-smart-open-line-above)
         ("C-<return>"   . crux-smart-open-line)
         ("M-S-<down>"   . crux-duplicate-current-line-or-region)))

Buffers & Navigation

Killing Buffers

Sometimes, I’m putting some work away and I don’t want those files to show up in the buffer list. Killing a buffer with C-x k or marking several buffers in the buffer list to kill them is fine, but can be a bit cumbersome.

I found this function in a Stack Exchange answer. It allows me to close the current buffer easily by pressing C-c k. If I prefix it, by writing C-u C-c k, then all “interesting” buffers are killed, leaving internal Emacs buffers intact. This cleans up all the buffers I’ve opened or used myself.

(defun soph/kill-buffer (&optional arg)
"When called with a prefix argument -- i.e., C-u -- kill all interesting
buffers -- i.e., all buffers without a leading space in the buffer-name.
When called without a prefix argument, kill just the current buffer
-- i.e., interesting or uninteresting."
(interactive "P")
  (cond
    ((and (consp arg) (equal arg '(4)))
      (mapc
        (lambda (x)
          (let ((name (buffer-name x)))
            (unless (eq ?\s (aref name 0))
              (kill-buffer x))))
        (buffer-list)))
    (t
      (kill-buffer (current-buffer)))))

(define-key custom-bindings-map (kbd "C-c k") 'soph/kill-buffer)

Kill Buffer and its Associated File

This function is from the blog What the .emacs.d!?. It deletes the file opened in your buffer and kills the buffer.

(defun magnar/delete-current-buffer-file ()
  "Removes file connected to current buffer and kills buffer."
  (interactive)
  (let ((filename (buffer-file-name))
        (buffer (current-buffer))
        (name (buffer-name)))
    (if (not (and filename (file-exists-p filename)))
        (ido-kill-buffer)
      (when (yes-or-no-p "Are you sure you want to remove this file? ")
        (delete-file filename)
        (kill-buffer buffer)
        (message "File '%s' successfully removed" filename)))))

Renaming Buffer and its Associated File

This function is also from What the .emacs.d!?. It renames the current buffer and its associated file, all in one go.

(defun magnar/rename-current-buffer-file ()
  "Renames current buffer and file it is visiting."
  (interactive)
  (let ((name (buffer-name))
        (filename (buffer-file-name)))
    (if (not (and filename (file-exists-p filename)))
        (error "Buffer '%s' is not visiting a file!" name)
      (let ((new-name (read-file-name "New name: " filename)))
        (if (get-buffer new-name)
            (error "A buffer named '%s' already exists!" new-name)
          (rename-file filename new-name 1)
          (rename-buffer new-name)
          (set-visited-file-name new-name)
          (set-buffer-modified-p nil)
          (message "File '%s' successfully renamed to '%s'"
                   name (file-name-nondirectory new-name)))))))

Splitting Windows

I want maximum two windows by default. I have a function, taken from this Stack Overflow post, that rewrites the split-window-sensibly function to reverse its preference and essentially prefer splitting side-by-side.

(defun split-window-sensibly-prefer-horizontal (&optional window)
"Based on `split-window-sensibly', but prefers to split WINDOW side-by-side."
  (let ((window (or window (selected-window))))
    (or (and (window-splittable-p window t)
         ;; Split window horizontally
         (with-selected-window window
           (split-window-right)))
    (and (window-splittable-p window)
         ;; Split window vertically
         (with-selected-window window
           (split-window-below)))
    (and
         ;; If WINDOW is the only usable window on its frame (it is
         ;; the only one or, not being the only one, all the other
         ;; ones are dedicated) and is not the minibuffer window, try
         ;; to split it horizontally disregarding the value of
         ;; `split-height-threshold'.
         (let ((frame (window-frame window)))
           (or
            (eq window (frame-root-window frame))
            (catch 'done
              (walk-window-tree (lambda (w)
                                  (unless (or (eq w window)
                                              (window-dedicated-p w))
                                    (throw 'done nil)))
                                frame)
              t)))
     (not (window-minibuffer-p window))
     (let ((split-width-threshold 0))
       (when (window-splittable-p window t)
         (with-selected-window window
               (split-window-right))))))))

(defun split-window-really-sensibly (&optional window)
  (let ((window (or window (selected-window))))
    (if (> (window-total-width window) (* 2 (window-total-height window)))
        (with-selected-window window (split-window-sensibly-prefer-horizontal window))
      (with-selected-window window (split-window-sensibly window)))))

(setq split-window-preferred-function 'split-window-really-sensibly)

If I have already split the frame into two windows and then call a function that opens a new window (for example a Magit or a compilation buffer), then I want Emacs to reuse the inactive window instead of creating a new one. Setting both split-height-threshold and split-width-threshold to nil seems to ensure this.

(setq-default split-height-threshold nil
              split-width-threshold  nil
              fill-column            80) ;; Maximum line width
              ;; window-min-width       80) ;; No smaller windows than this

Opening, Closing, & Switching Windows

Opening, switching and deleting windows becomes super easy with switch-window.

(use-package switch-window
  :bind (:map custom-bindings-map
              ("C-x o" . 'switch-window)
              ("C-x 1" . 'switch-window-then-maximize)
              ("C-x 2" . 'switch-window-then-split-below)
              ("C-x 3" . 'switch-window-then-split-right)
              ("C-x 0" . 'switch-window-then-delete)))

I often need to switch back and forth between the current and the last opened buffer, which usually takes three keystrokes: C-x b RET. Let’s bind it to C-. for convenience, with a function I got from What the .emacs.d!?.

(fset 'quick-switch-buffer [?\C-x ?b return])
(define-key custom-bindings-map (kbd "C-.") 'quick-switch-buffer)

And Transpose Frame has some nice functions for shifting windows around. I only really use the one to swap the left- and right-hand sides of the frame, but there are others you might find useful.

(use-package transpose-frame
  :bind (:map custom-bindings-map
              ("C-c f" . 'flop-frame)))

Project Management

Projectile provides a convenient project interaction interface. I keep most of my projects in a specific folder, so I’ll set Projectile to check that path specifically.

(use-package projectile
  :defer t
  :bind (:map custom-bindings-map ("C-c p" . projectile-command-map))
  :config
  (setq projectile-project-search-path '("~/Dropbox/projects/"))
  (projectile-mode))

Dired

Emacs’s default file manager is nice, but contains a bit more info than I usually need. dired-hide-details-mode does what it says on the tin, and I can easily activate/deactivate it on the fly with the default keybinding, (.

I’ll also bind a few convenience keys. C- followed by an arrow moves into a directory/open a file or move up a directory. And lowercase c creates/touches a new file and prompts for a name.

The last line is a setting for MacOS telling it to use gls when using dired.

(use-package dired
  :ensure nil
  :hook (dired-mode . dired-hide-details-mode)
  :bind (:map dired-mode-map
              ("C-<right>" . dired-find-alternate-file)
              ("C-<left>"  . dired-up-directory)
              ("C-<down>"  . dired-find-alternate-file)
              ("C-<up>"    . dired-up-directory)
              ("c"         . dired-create-empty-file))
  :config
  (when (and (eq system-type 'darwin) (executable-find "gls"))
    (setq dired-use-ls-dired nil)))

From this StackOverflow post.

(put 'dired-find-alternate-file 'disabled nil) ; disables warning
(define-key dired-mode-map (kbd "RET") 'dired-find-alternate-file) ; was dired-advertised-find-file
(define-key dired-mode-map (kbd "^") (lambda () (interactive) (find-alternate-file "..")))  ; was dired-up-directory

Completion

Emacs distinguishes between two different kinds of completion: complete-at-point (text/code autocomlete) and completing-read (completion of Emacs commands, file names, etc.).

For completing-read, I use Vertico and for completion-at-point at use Company. I also use a few complimentary packages that enhance the experience.

Vertico

Vertico is heart of this completion UI!

I’ll use the function from this What the .emacs.d!? post which lets me type ~ at the Vertico prompt to go directly to the home directory. For use with Vertico, I add a call to delete-minibuffer-contents so that old path is cleared before starting the new file path (starting at ~/).

(defun soph/take-me-home ()
  (interactive)
  (if (looking-back "/" nil)
      (progn (call-interactively 'delete-minibuffer-contents) (insert "~/"))
    (call-interactively 'self-insert-command)))

(use-package vertico
  :defer t
  :bind (:map vertico-map ("~" . soph/take-me-home))
  :config
  (vertico-mode)
  (vertico-multiform-mode)
  (setq read-extended-command-predicate       'command-completion-default-include-p
        vertico-count                         28  ; Show more candidates
        read-file-name-completion-ignore-case t   ; Ignore case of file names
        read-buffer-completion-ignore-case    t   ; Ignore case in buffer completion
        completion-ignore-case                t)) ; Ignore case in completion

Vertico Posframe

vertico-posframe makes Vertico appear in a small child frame, instead of as a traditional minibuffer. I like to have mine in the middle of the frame, with small fringes on either side.

I temporarily disable vertico-posframe-mode when searching with consult. When selecting a search match, a preview is provided. That’s kind of hard to see with the posframe in the middle of the screen, so while searching I just use the normal minibuffer.

(use-package vertico-posframe
  :init
  (setq vertico-posframe-parameters   '((left-fringe  . 12)    ;; Fringes
                                        (right-fringe . 12)
                                        (undecorated  . nil))) ;; Rounded frame
  :config
  (vertico-posframe-mode 1)
  (setq vertico-posframe-width        88                       ;; Narrow frame
        vertico-posframe-height       vertico-count            ;; Default height
        ;; Don't create posframe for these commands
        vertico-multiform-commands    '((consult-line    (:not posframe))
                                        (consult-ripgrep (:not posframe)))))

The rounded frame corners (putting (undecorated . nil) in the vertico-posframe-parameters) look really nice on Mac OS.

./images/vertico-posframe-screenshot.png

Orderless

And Orderless is a package for a completion style, that matches multiple regexes, in any order.

(use-package orderless
  :ensure t
  :config
  (setq completion-styles '(orderless basic partial-completion)
        completion-category-overrides '((file (styles basic partial-completion)))
        orderless-component-separator "[ |]"))

Company

Company (COMPlete ANYthing) is a battle-tested completion package that works really well with LSP-mode.

(use-package company
  :config
  (setq company-idle-delay                 0.0
        company-minimum-prefix-length      2
        company-tooltip-align-annotations  t
        company-tooltip-annotation-padding 1
        company-tooltip-margin             1
        company-detect-icons-margin        'company-dot-icons-margin)
  (global-company-mode t))

Search

Search Utilities

Projectile also comes with a ton of built-in functionality to search in your projects. Other packages I use also depend on search utilities.

I use both ripgrep and ag (The Silver Searcher). wgrep also comes in handy sometimes. I’ll install all the corresponding Emacs packages.

(use-package ripgrep
  :defer t)

(use-package rg
  :defer t)

(use-package ag
  :defer t)

(use-package wgrep
  :defer t)

I want to use ripgrep as grep.

(setq grep-command "rg -nS --no-heading "
      grep-use-null-device nil)

Consult

Consult provides a ton of search, navigation, and completion functionality. I would definitely recommend looking at the documentation to learn more about all that it can do.

I often press C-x C-b when I only mean to press C-x b. If I want to open the list of all buffers, I’ll call it with M-x list-buffers, so let’s rebind this one to the same as C-x b so save me some grief.

(use-package consult
  :bind (:map custom-bindings-map
              ("C-s"     . consult-line)
              ("C-M-s"   . consult-ripgrep)
              ("C-x b"   . consult-buffer)
              ("C-x C-b" . consult-buffer)
              ("M-g g"   . consult-goto-line)
              ("M-g t"   . consult-imenu)
              ("M-g a"   . consult-imenu-multi)))

Imenu List

Imenu is a built-in Emacs utility that gives you a minibuffer of the symbols in the current buffer and let’s you jump to it. imenu-list is a nice package that gives you a new buffer with a navigable list of the functions, vars, etc. in your buffer, allowing you to quickly get an overview or jump to definition.

(use-package imenu-list
  :defer t
  :bind (:map custom-bindings-map
              ("M-g i" . imenu-list-smart-toggle)))

Marginalia

Marginalia gives me annotations in the minibuffer.

(use-package marginalia
  :init 
  (marginalia-mode 1))

Misc. Packages

Terminal Emulator

vterm

I like vterm and usually just use that. I don’t want it to double check with me before killing an instance of the terminal, so I’ll set it to just kill it. I also really Lars’ vterm functions, so I’ll use those as well. One is for toggling the vterm buffer with the other open buffer, and another binds a separate vterm instance to each M-n keystroke.

Lastly, deleting whole words doesn’t work well in vterm by default, so if anyone has a good tip for how to overwrite my custom bindings map in just vterm, please do let me know :~)

(use-package vterm
  :defer t

  :preface
  (let ((last-vterm ""))
    (defun toggle-vterm ()
      (interactive)
      (cond ((string-match-p "^\\vterm<[1-9][0-9]*>$" (buffer-name))
             (goto-non-vterm-buffer))
            ((get-buffer last-vterm) (switch-to-buffer last-vterm))
            (t (vterm (setq last-vterm "vterm<1>")))))

    (defun goto-non-vterm-buffer ()
      (let* ((r "^\\vterm<[1-9][0-9]*>$")
             (vterm-buffer-p (lambda (b) (string-match-p r (buffer-name b))))
             (non-vterms (cl-remove-if vterm-buffer-p (buffer-list))))
        (when non-vterms
          (switch-to-buffer (car non-vterms)))))

	(defun switch-vterm (n)
      (let ((buffer-name (format "vterm<%d>" n)))
        (setq last-vterm buffer-name)
        (cond ((get-buffer buffer-name)
               (switch-to-buffer buffer-name))
              (t (vterm buffer-name)
                 (rename-buffer buffer-name))))))

  :bind (:map custom-bindings-map
              ("C-z" . toggle-vterm)
              ("M-1" . (lambda () (interactive) (switch-vterm 1)))
              ("M-2" . (lambda () (interactive) (switch-vterm 2)))
              ("M-3" . (lambda () (interactive) (switch-vterm 3)))
              ("M-4" . (lambda () (interactive) (switch-vterm 4)))
              ("M-5" . (lambda () (interactive) (switch-vterm 5)))
              ("M-6" . (lambda () (interactive) (switch-vterm 6)))
              ("M-7" . (lambda () (interactive) (switch-vterm 7)))
              ("M-8" . (lambda () (interactive) (switch-vterm 8)))
              ("M-9" . (lambda () (interactive) (switch-vterm 9))))
  :bind (:map vterm-mode-map
			  ("C-c C-c" . (lambda () (interactive) (vterm-send-key (kbd "C-c")))))

  :config
  ;; Don't query about killing vterm buffers, just kill it
  (defun my-vterm-kill-with-no-query (&rest _)
    "Set process query on exit flag to nil for vterm buffer."
    (set-process-query-on-exit-flag (get-buffer-process (current-buffer)) nil))

  (advice-add 'vterm :after #'my-vterm-kill-with-no-query))

Version Control (Magit & Friends)

Magit is a Git client specifically for Emacs, and it’s super powerful. It’s the centre of all my version control packages.

Git Gutter with diff-hl

Let’s first make sure we’re highlighting uncommitted changes with diff-hl. It highlights added, deleted, and modified code segments by adding a coloured bar to the left-hand gutter of the buffer.

(use-package diff-hl
  :config
  (global-diff-hl-mode))

Magit

Then configure Magit. I’ll add hooks to have diff-hl update the gutter whenever Magit refreshes.

(use-package magit
  :defer t
  :hook
  ((magit-pre-refresh  . diff-hl-magit-pre-refresh)
   (magit-post-refresh . diff-hl-magit-post-refresh))
  :config
  (setq magit-mode-quit-window 'magit-restore-window-configuration
		magit-auto-revert-mode t))

Magit TODOs

I’ll use magit-todos to show the project’s TODOs directly in the Magit buffer.

(use-package magit-todos
  :after magit
  :config
  (magit-todos-mode 1))

Magit Forge

And Magit Forge to be able to work with Git forges (e.g., GitHub, and GitLab) directly from Magit.

(use-package forge
  :after magit)

Blamer

Blamer is a Git blame plugin, inspired by VS Code’s GitLens Plugin, which gives you blame info to the right of the selected line(s) as an overlay. You can also pop the info out into a pos-frame, which works pretty well for reading PR discussions. I find this slightly more ergonomic than Magit’s magit-blame-addition.

(use-package blamer
  :after magit
  :bind (("C-c g i" . blamer-show-commit-info)
         ("C-c g b" . blamer-show-posframe-commit-info))
  :defer 20
  :custom
  (blamer-idle-time                 0.3)
  (blamer-min-offset                4)
  (blamer-max-commit-message-length 100)
  (blamer-datetime-formatter        "[%s]")
  (blamer-commit-formatter          " ● %s")
  :custom-face
  (blamer-face ((t :foreground "#7aa2cf"
                    :background nil
                    :height 1
                    :italic nil))))

Git Link

git-link creates URL links to the current position in your buffer in the corresponding forge repo. Super handy for sending to others.

(use-package git-link
  :defer t
  :init
  (setq git-link-use-commit t
        git-link-open-in-browser t))

Git Timemachine

Git Time Machine lets you step through different versions of a Git-controlled file directly in the current buffer, without even needing to hop over to the Magit status buffer.

(use-package git-timemachine
  :defer t)

Trying Packages

Lars Tveito’s Try package lets you try out packages and only save them temporarily, saving you the hassle of cleaning up afterwards if you decide you don’t want to keep using the package. You can even try packages from .el files from URLs directly.

(use-package try)

Snippets

YASnippet is a template system for Emacs that allows you to predefine snippets you use often and insert them easily. I want snippets for basic Org-files, Roam-notes, and other sequences often used.

(use-package yasnippet
  :diminish yas-minor-mode
  :defer 5
  :config
  (setq yas-snippet-dirs '("~/.emacs.d/snippets/"))
  (yas-global-mode 1)) ;; or M-x yas-reload-all if you've started YASnippet already.

;; Silences the warning when running a snippet with backticks (runs a command in the snippet)
(require 'warnings)
(add-to-list 'warning-suppress-types '(yasnippet backquote-change)) 

Better Help Buffers

Helpful is an improvement on Emacs’ built-in help buffer. It’s more user-friendly and easier to read.

(use-package helpful
  :bind (:map custom-bindings-map
			  ("C-h f" . #'helpful-function)
			  ("C-h v" . #'helpful-variable)
			  ("C-h k" . #'helpful-key)
			  ("C-h x" . #'helpful-command)
			  ("C-h d" . #'helpful-at-point)
			  ("C-h c" . #'helpful-callable)))

which-key shows you available keybindings in the minibuffer. When you’ve started to enter a command, it will show you where you can go from there.

(use-package which-key
  :config
  (which-key-mode))

Jinx Spellchecker

Jinx is a libenchant-powered spellchecker with a super nice UI. I’m trying it out instead of Flyspell, which I used before.

(use-package jinx
  :hook (emacs-startup . global-jinx-mode)
  :bind (("M-$"   . jinx-correct)
         ("C-M-$" . jinx-languages))
  :config
  (setq jinx-languages "en_GB"))

LaTeX

I use AUCTeX to work with LaTeX files from within Emacs and it’s a massive help. It has a lot of different features, and I’d recommend checking out the documentation to see all the stuff you can do with it.

I also really like reftex-mode, which gives you a table of contents with clickable links for your file with the keybinding C-c =.

(use-package auctex
  :hook
  (LaTeX-mode . turn-on-prettify-symbols-mode)
  (LaTeX-mode . reftex-mode)
  (LaTeX-mode . (lambda () (corfu-mode -1)))
  (LaTeX-mode . outline-minor-mode)
  (LaTeX-mode . olivetti-mode))

When the reftex window opens, I want it on the left side of the screen and I want it to take up less than half the screen.

(setq reftex-toc-split-windows-horizontally t
	  reftex-toc-split-windows-fraction     0.2)

PDF Tools

PDF Tools is an improved version of the built-in DocView for viewing PDFs. It has extensive features, but does not play well with consult, so I’ll rebind C-s to isearch-forward.

(use-package pdf-tools
  :defer t
  :init (pdf-loader-install)
  :hook ((pdf-view-mode . (lambda () (auto-revert-mode -1)))
         (pdf-view-mode . (lambda () (corfu-mode -1)))
         (pdf-view-mode . (lambda () (company-mode -1))))
  :bind (:map pdf-view-mode-map
              ("C-s"   . isearch-forward)
              ("C-M-s" . pdf-occur)))

Warn me when a PDF has been opened with the default DocView mode instead of PDF Tools’ PDF View mode.

(use-package doc-view
  :hook (doc-view-mode . (lambda ()
                           (display-warning
                            emacs
                            "Oops, using DocView instead of PDF Tools!"
                            :warning))))

saveplace-pdf-view is a great package that remembers where in your PDFs you last left off, down to the scroll position and zoom amount.

(use-package pdf-view-restore
  :after pdf-tools
  :config
  (add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode))

EPUBs

nov.el is a package for reading EPUBs (an e-book format) directly in Emacs.

(use-package nov
  :defer t
  :config
  (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode)))

Editor Config

I want to use the EditorConfig plugin, which helps maintain consistent coding styles across editors when collaborating.

(use-package editorconfig
  :defer t)

Browser Preference

Open links with Firefox by default.

(when (eq system-type 'darwin)
  (setq browse-url-browser-function 'browse-url-default-macosx-browser))

(when (eq system-type 'gnu/linux)
  (setq browse-url-browser-function 'browse-url-generic
		browse-url-generic-program "firefox"))

Elfeed

Elfeed is a feed reader for Emacs!

(use-package elfeed
  :bind (:map custom-bindings-map ("C-x w" . elfeed))
  :config
  (setq elfeed-feeds
      '("http://nullprogram.com/feed/"
        "https://planet.emacslife.com/atom.xml"
        "https://deniskyashif.com/index.xml"
        "https://sophiebos.io/index.xml")))

Config Profiling

ESUP is a package for profiling your config. You can use it to shave precious seconds off your startup time, which is useful to me because I keep closing it when I’m done with a task and then immediately needing it again.

(use-package esup
  :defer t
  :config
  (setq esup-depth 0))

Org

Org Mode is a smart text system that is used for organising notes, literate programming, time management, and a wide variety of other use cases. I’ve been interested in switching from my previous note-taking app, Obsidian, to using Org and Roam (described in the next section).

Let’s first make sure we’re using Org. Note that I am leaving the last parenthesis open, to include some options from the “Visuals” section inside the use-package declaration for Org mode.

(use-package org
  :defer t

Visuals

Text Centring

Note: We are still in the :config section of the use-package declaration for Org mode.

I always want to center the text and enable linebreaks in Org. I’ve added a hook to activate olivetti-mode, and visual-fill-mode is always on.

:hook (org-mode . olivetti-mode)

Fonts

Note: We are in the :config section of the use-package declaration for Org mode.

Set the sizes and fonts for the various headings.

:config
;; Resize Org headings
(custom-set-faces
'(org-document-title ((t (:height 1.6))))
'(outline-1          ((t (:height 1.25))))
'(outline-2          ((t (:height 1.2))))
'(outline-3          ((t (:height 1.15))))
'(outline-4          ((t (:height 1.1))))
'(outline-5          ((t (:height 1.1))))
'(outline-6          ((t (:height 1.1))))
'(outline-8          ((t (:height 1.1))))
'(outline-9          ((t (:height 1.1)))))

LaTeX Previews

Note: We are in the :config section of the use-package declaration for Org mode.

Preview LaTeX fragments by default.

(setq org-startup-with-latex-preview t)

Increase the size of LaTeX previews in Org.

(plist-put org-format-latex-options :scale 1.35)

I’ve been struggling a little to get LaTeX previews to work on my work Mac. I symlinked my LaTeX texbin directory to /usr/local/bin, and it still didn’t work. Eventually I found this Stack Exchange post that correctly diagnosed the issue.

(let ((png (cdr (assoc 'dvipng org-preview-latex-process-alist))))
    (plist-put png :latex-compiler '("latex -interaction nonstopmode -output-directory %o %F"))
    (plist-put png :image-converter '("dvipng -D %D -T tight -o %O %F"))
    (plist-put png :transparent-image-converter '("dvipng -D %D -T tight -bg Transparent -o %O %F")))

Folded Startup

Note: We are still in the :config section of the use-package declaration for Org mode.

In general, show me all the headings.

(setq org-startup-folded 'content)

Decluttering

Note: We are still in the :config section of the use-package declaration for Org mode.

We’ll declutter by adapting the indentation and hiding leading starts in headings. We’ll also use “pretty entities”, which allow us to insert special characters LaTeX-style by using a leading backslash (e.g., \alpha to write the greek letter alpha) and display ellipses in a condensed way.

(setq org-adapt-indentation t
      org-hide-leading-stars t
      org-pretty-entities t
      org-ellipsis "  ·")

For source code blocks specifically, I want Org to display the contents using the major mode of the relevant language. I also want TAB to behave inside the source code block like it normally would when writing code in that language.

(setq org-src-fontify-natively t
      org-src-tab-acts-natively t
      org-edit-src-content-indentation 0)

Some Org options to deal with headers and TODO’s nicely.

(setq org-log-done                       t
      org-auto-align-tags                t
      org-tags-column                    -80
      org-fold-catch-invisible-edits     'show-and-error
      org-special-ctrl-a/e               t
      org-insert-heading-respect-content t)

Let’s finally close the use-package declaration with a parenthesis.

)

Hide Emphasis Markers

Many people hide emphasis markers (e.g., /.../ for italics, *...* for bold, etc.) to have a cleaner visual look, but I got frustrated trying to go back and edit text in these markers, as sometimes I would delete the markers itself or write outside the markers. org-appear is the solution to all my troubles. It displays the markers when the cursor is within them and hides them otherwise, making edits easy while looking pretty.

(use-package org-appear
  :commands (org-appear-mode)
  :hook     (org-mode . org-appear-mode)
  :config 
  (setq org-hide-emphasis-markers t)  ;; Must be activated for org-appear to work
  (setq org-appear-autoemphasis   t   ;; Show bold, italics, verbatim, etc.
        org-appear-autolinks      t   ;; Show links
        org-appear-autosubmarkers t)) ;; Show sub- and superscripts

Inline Images

Show inline images by default

(setq org-startup-with-inline-images t)

Variable Pitch

Make sure variable-pitch-mode is always active in Org buffers. I normally wouldn’t need this, since I use the mixed-pitch package in the font section, but for some reason, it seems the header bullet in Org mode are affected by this.

(add-hook 'org-mode-hook 'variable-pitch-mode)

LaTeX Fragtog

org-fragtog works like org-appear, but for LaTeX fragments: It toggles LaTeX previews on and off automatically, depending on the cursor position. If you move the cursor to a preview, it’s toggled off so you can edit the LaTeX snippet. When you move the cursor away, the preview is turned on again.

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

Bullets

org-superstar styles some of my UI elements, such as bullets and special checkboxes for TODOs.

(use-package org-superstar
  :after org
  :config
  (setq org-superstar-leading-bullet " ")
  (setq org-superstar-headline-bullets-list '("" "" "" "" "" ""))
  (setq org-superstar-special-todo-items t) ;; Makes TODO header bullets into boxes
  (setq org-superstar-todo-bullet-alist '(("TODO"  . 9744)
                                          ("PROG"  . 9744)
                                          ("NEXT"  . 9744)
                                          ("WAIT"  . 9744)
                                          ("DONE"  . 9745)))
  :hook (org-mode . org-superstar-mode))

SVG Elements

svg-tag-mode lets you replace keywords such as TODOs, tags, and progress bars with nice SVG graphics. I use it for dates, progress bars, and citations.

(use-package svg-tag-mode
  :after org
  :config
  (defconst date-re "[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}")
  (defconst time-re "[0-9]\\{2\\}:[0-9]\\{2\\}")
  (defconst day-re "[A-Za-z]\\{3\\}")
  (defconst day-time-re (format "\\(%s\\)? ?\\(%s\\)?" day-re time-re))

  (defun svg-progress-percent (value)
	(svg-image (svg-lib-concat
				(svg-lib-progress-bar (/ (string-to-number value) 100.0)
			      nil :margin 0 :stroke 2 :radius 3 :padding 2 :width 11)
				(svg-lib-tag (concat value "%")
				  nil :stroke 0 :margin 0)) :ascent 'center))

  (defun svg-progress-count (value)
	(let* ((seq (mapcar #'string-to-number (split-string value "/")))
           (count (float (car seq)))
           (total (float (cadr seq))))
	  (svg-image (svg-lib-concat
				  (svg-lib-progress-bar (/ count total) nil
					:margin 0 :stroke 2 :radius 3 :padding 2 :width 11)
				  (svg-lib-tag value nil
					:stroke 0 :margin 0)) :ascent 'center)))
  (setq svg-tag-tags
      `(;; Org tags
        ;; (":\\([A-Za-z0-9]+\\)" . ((lambda (tag) (svg-tag-make tag))))
        ;; (":\\([A-Za-z0-9]+[ \-]\\)" . ((lambda (tag) tag)))
        
        ;; Task priority
        ("\\[#[A-Z]\\]" . ( (lambda (tag)
                              (svg-tag-make tag :face 'org-priority 
                                            :beg 2 :end -1 :margin 0))))

        ;; Progress
        ("\\(\\[[0-9]\\{1,3\\}%\\]\\)" . ((lambda (tag)
          (svg-progress-percent (substring tag 1 -2)))))
        ("\\(\\[[0-9]+/[0-9]+\\]\\)" . ((lambda (tag)
          (svg-progress-count (substring tag 1 -1)))))
        
        ;; TODO / DONE
        ;; ("TODO" . ((lambda (tag) (svg-tag-make "TODO" :face 'org-todo
		;; 									           :inverse t :margin 0))))
        ;; ("DONE" . ((lambda (tag) (svg-tag-make "DONE" :face 'org-done :margin 0))))


        ;; Citation of the form [cite:@Knuth:1984] 
        ("\\(\\[cite:@[A-Za-z]+:\\)" . ((lambda (tag)
                                          (svg-tag-make tag
                                                        :inverse t
                                                        :beg 7 :end -1
                                                        :crop-right t))))
        ("\\[cite:@[A-Za-z]+:\\([0-9]+\\]\\)" . ((lambda (tag)
                                                (svg-tag-make tag
                                                              :end -1
                                                              :crop-left t))))

        
        ;; Active date (with or without day name, with or without time)
        (,(format "\\(<%s>\\)" date-re) .
         ((lambda (tag)
            (svg-tag-make tag :beg 1 :end -1 :margin 0))))
        (,(format "\\(<%s \\)%s>" date-re day-time-re) .
         ((lambda (tag)
            (svg-tag-make tag :beg 1 :inverse nil :crop-right t :margin 0))))
        (,(format "<%s \\(%s>\\)" date-re day-time-re) .
         ((lambda (tag)
            (svg-tag-make tag :end -1 :inverse t :crop-left t :margin 0))))

        ;; Inactive date  (with or without day name, with or without time)
         (,(format "\\(\\[%s\\]\\)" date-re) .
          ((lambda (tag)
             (svg-tag-make tag :beg 1 :end -1 :margin 0 :face 'org-date))))
         (,(format "\\(\\[%s \\)%s\\]" date-re day-time-re) .
          ((lambda (tag)
             (svg-tag-make tag :beg 1 :inverse nil :crop-right t :margin 0 :face 'org-date))))
         (,(format "\\[%s \\(%s\\]\\)" date-re day-time-re) .
          ((lambda (tag)
             (svg-tag-make tag :end -1 :inverse t :crop-left t :margin 0 :face 'org-date)))))))

(add-hook 'org-mode-hook 'svg-tag-mode)

Prettify Tags & Keywords

I have a custom function to prettify tags and other elements, lifted from Jake B’s Emacs setup.

(defun soph/prettify-symbols-setup ()
  "Beautify keywords"
  (setq prettify-symbols-alist
		(mapcan (lambda (x) (list x (cons (upcase (car x)) (cdr x))))
				'(; Greek symbols
				  ("lambda" . )
				  ("delta"  . )
				  ("gamma"  . )
				  ("phi"    . )
				  ("psi"    . )
				  ; Org headers
				  ("#+title:"  . "")
				  ("#+author:" . "")
                  ("#+date:"   . "")
				  ; Checkboxes
				  ("[ ]" . "")
				  ("[X]" . "")
				  ("[-]" . "")
				  ; Blocks
				  ("#+begin_src"   . "") ;
				  ("#+end_src"     . "")
				  ("#+begin_QUOTE" . "")
				  ("#+begin_QUOTE" . "")
				  ; Drawers
				  ;    ⚙️
				  (":properties:" . "")
				  ; Agenda scheduling
				  ("SCHEDULED:"   . "🕘")
				  ("DEADLINE:"    . "")
				  ; Agenda tags  
				  (":@projects:"  . "")
				  (":work:"       . "🚀")
				  (":@inbox:"     . "✉️")
				  (":goal:"       . "🎯")
				  (":task:"       . "📋")
				  (":@thesis:"    . "📝")
				  (":thesis:"     . "📝")
				  (":uio:"        . "🏛️")
				  (":emacs:"      . "")
				  (":learn:"      . "🌱")
				  (":code:"       . "💻")
				  (":fix:"        . "🛠️")
				  (":bug:"        . "🚩")
				  (":read:"       . "📚")
				  ; Roam tags
				  ("#+filetags:"  . "📎")
				  (":wip:"        . "🏗️")
				  (":ct:"         . "➡️") ; Category Theory
                  ; ETC
                  (":verb:"       . "🌐") ; HTTP Requests in Org mode
				  )))
  (prettify-symbols-mode))

(add-hook 'org-mode-hook        #'soph/prettify-symbols-setup)
(add-hook 'org-agenda-mode-hook #'soph/prettify-symbols-setup)

Right-Align Tags

Code snippet from this Reddit post. It actually right-aligns tags, using font-lock and the display property.

(add-to-list 'font-lock-extra-managed-props 'display)
(font-lock-add-keywords 'org-mode
                        `(("^.*?\\( \\)\\(:[[:alnum:]_@#%:]+:\\)$"
                           (1 `(face nil
                                     display (space :align-to (- right ,(org-string-width (match-string 2)) 3)))
                              prepend))) t)

General Interaction

Disable Electric Indent Mode

The built-in electric indent mode is great - just not for Org mode.

(add-hook 'org-mode-hook #'(lambda () (electric-indent-local-mode -1)))

Opening Links

By default, when opening an Org-link, the current window is split into two. I’d like for the new window to replace the current one. To do this, we need to edit org-link-frame-setup and change the default cons (file . find-file-other-window) to (file . find-file).

(setq org-link-frame-setup
      '((vm      . vm-visit-folder-other-frame)
        (vm-imap . vm-visit-imap-folder-other-frame)
        (gnus    . org-gnus-no-new-news)
        (file    . find-file)
        (wl      . wl-other-frame)))

I’d also like to open links with RET.

(setq org-return-follows-link t)

Editing

Don’t insert a blank newline before new entries (e.g., list bullets and section headings). I find it annoying when I want to insert a new task under the current one in my agenda if there’s a blank newline between the previous entry and the next.

(setq org-blank-before-new-entry '((heading . nil)
                                   (plain-list-item . nil)))

Agenda

First, some regular agenda settings.

I want to open my agenda on the current day, not on any specific weekday.

I also don’t want to have a divider line separating my different agenda blocks. This is because I sometimes use packages like Olivetti to center the agenda, which makes the divider line wrap around and take up multiple lines.

Similarly, I right-align my tags, so they also end up shifted around and often on a new line. org-agenda-remove-tags doesn’t remove them, but for some reason it disables the right-alignment in the agenda, which is perfect.

(setq org-agenda-start-on-weekday nil
      org-agenda-block-separator  nil
      org-agenda-remove-tags      t)

Super Agenda

org-super-agenda lets you group agenda items into sections, so it’s easier to navigate.

(use-package org-super-agenda
  :after org
  :config
  (setq org-super-agenda-header-prefix "\n")
  ;; Hide the thin width char glyph
  (add-hook 'org-agenda-mode-hook
            #'(lambda () (setq-local nobreak-char-display nil)))
  (org-super-agenda-mode))

Org QL

org-ql is a query language for Org mode. It’s super powerful and doesn’t really belong in the Agenda section of my config, but for now, I only use it to find things and to set up a pretty calendar view.

One of the things I want to find regularly, is a list of all my TODOs marked with the custom state QUESTION. Usually, this is stuff that I want to bring up in my next meeting with someone, so it’s handy to be able to pull up all the questions I have. org-ql is perfect for that.

(use-package org-ql
  :after org
  :config
  (add-to-list 'org-ql-views
             '("Questions" :buffers-files org-agenda-files :query
               (and
                (not
                 (done))
                (todo "QUESTION"))
               :sort
               (todo priority date)
               :super-groups org-super-agenda-groups :title "Agenda-like")))

Agenda Views

With Super Agenda and Org QL, we can now define some display groups for the agenda, to show us exactly the info we want.

We’ll set up some groups with the Super Agenda syntax.

;; Delete default agenda commands
(setq org-agenda-custom-commands nil)

(defvar regular-view-groups
  '((:name "Scheduled"
     :scheduled t
     :order 1)
	(:name "Deadlines"
     :deadline t
     :order 2)))

Now I’ll set up commands to open the day view with C-c a d and extended three-day view with C-c a e. Notice that I’m first setting some options for the built-in agenda, and then defining a block with Super Agenda groups and Org QL queries.

(add-to-list 'org-agenda-custom-commands
	  '("d" "Day View"
		 ((agenda "" ((org-agenda-overriding-header "Day View")
                      (org-agenda-span 'day)
                      (org-super-agenda-groups regular-view-groups)))
		  (org-ql-block '(todo "PROG") ((org-ql-block-header "\n❯ In Progress")))
		  (org-ql-block '(todo "NEXT") ((org-ql-block-header "\n❯ Next Up")))
          (org-ql-block '(todo "WAIT") ((org-ql-block-header "\n❯ Waiting")))
		  (org-ql-block '(priority "A") ((org-ql-block-header "\n❯ Important"))))))


(add-to-list 'org-agenda-custom-commands
		'("e" "Three-Day View"
               ((agenda "" ((org-agenda-span 3)
                            (org-agenda-start-on-weekday nil)
                            (org-deadline-warning-days 0))))))

Displaying Scheduled & Deadline Items

Don’t show me deadlines or scheduled items if they are done.

(setq org-agenda-skip-deadline-if-done  t
	  org-agenda-skip-scheduled-if-done t)

Modify dealine leader text.

(setq org-agenda-deadline-leaders '("Deadline:  " "In %2d d.: " "%2d d. ago: "))

Tasks

Task Priorities

Let’s increase the number of possible priorities for Org tasks. I’ll set mine to E so that we have A through E, in total five levels.

(setq org-lowest-priority  ?F) ;; Gives us priorities A through F
(setq org-default-priority ?E) ;; If an item has no priority, it is considered [#E].

(setq org-priority-faces
      '((65 . "#BF616A")
        (66 . "#EBCB8B")
        (67 . "#B48EAD")
        (68 . "#81A1C1")
        (69 . "#5E81AC")
        (70 . "#4C566A")))

Custom TODO States

I’ll expand the list of default task states.

(setq org-todo-keywords
      '((sequence
         ;; Needs further action
		 "TODO(t)" "PROG(p)" "NEXT(n)" "WAIT(w)" "QUESTION(q)"
		 "|"
         ;; Needs no action currently
		 "DONE(d)")))

Mark As Done

Finally, to mark any TODO task, of any state, as DONE quickly, I have a helper function that I’ll bind to C-c d.

(defun org-mark-as-done ()
  (interactive)
  (save-excursion
    (org-back-to-heading t) ;; Make sure command works even if point is
                            ;; below target heading
    (cond ((looking-at "\*+ TODO")
           (org-todo "DONE"))
		  ((looking-at "\*+ NEXT")
           (org-todo "DONE"))
          ((looking-at "\*+ WAIT")
           (org-todo "DONE"))
		  ((looking-at "\*+ PROG")
           (org-todo "DONE"))
		  ((looking-at "\*+ DONE")
           (org-todo "DONE"))
          (t (message "Undefined TODO state.")))))

(define-key custom-bindings-map (kbd "C-c d") 'org-mark-as-done)

“Get Things Done” Setup

I’m trying out the Get Things Done method by David Allen, using Nicolas Rougier’s GTD configuration and Nicolas Petton’s blog post on the subject.

The first step is to set the relevant directories.

(setq org-directory "~/Dropbox/org/")
(add-to-list 'org-agenda-files "inbox.org")

Set the archive location to a unified archive.

(setq org-archive-location (concat org-directory "archive.org::"))

Then to set up the relevant capture templates, with accompanying keybindings.

(setq org-capture-templates
       `(("i" "Inbox" entry  (file "inbox.org")
        ,(concat "* TODO %?\n"
                 "/Entered on/ %U"))))
(defun org-capture-inbox ()
     (interactive)
     (call-interactively 'org-store-link)
     (org-capture nil "i"))

Keybindings

For basic agenda and TODO-related keybindings, I’ll use C-c followed by a single, lower-case letter.

(define-key custom-bindings-map (kbd "C-c l") 'org-store-link)
(define-key custom-bindings-map (kbd "C-c a") 'org-agenda)
(define-key custom-bindings-map (kbd "C-c c") 'org-capture)

(with-eval-after-load 'org
  (define-key org-mode-map (kbd "C-c t") 'org-todo))

For whatever reason, I’ve had an issue with clocking in, where the default keybinding used TAB instead of C-i to clock in, so I’ll set that manually.

(define-key org-mode-map (kbd "C-c C-x C-i") 'org-clock-in)

Registers

Registers are easier to access than bookmarks and much more flexible. I’ll set up registers for my GTD files.

(set-register ?i (cons 'file (concat org-directory "inbox.org")))
(set-register ?r (cons 'file (concat org-directory "roam/20240128135100-roam.org")))
(set-register ?p (cons 'file (concat org-directory "projects.org")))
(set-register ?c (cons 'file "~/Documents/playground/clj-playground/src/clj_playground/playground.clj"))
(set-register ?b (cons 'file "~/Dropbox/projects/blog/org-content/all-posts.org"))

Since I have C-s bound to consult-line which lets me search everywhere in a file, I don’t really need C-r to be bound to the default isearch-backward. Instead, I can use it as the leader key combination to jump to a register.

(define-key custom-bindings-map (kbd "C-r") 'jump-to-register)

Babel

For working with code blocks in Org mode, I want to make sure code blocks are not evaluated by default on export. I also want to add some languages.

(setq org-export-use-babel       nil
      org-confirm-babel-evaluate nil)
;; (org-babel-do-load-languages
;;  'org-babel-load-languages
;;  '((emacs-lisp . t)
;;    (python     . t)
;;    (haskell    . t)
;;    (clojure    . t)))

For Python, use whatever interpreter is set by python-shell-interpreter.

(use-package ob-python
  :ensure nil
  :after (ob python)
  :config
  (setq org-babel-python-command python-shell-interpreter))

Roam

Roam is a smart note-taking system in the style of a personal knowledge management system. org-roam is a port of this system that uses all plain-text Org-files.

I set up a Roam directory and added a simple configuration for navigating Roam nodes.

(use-package org-roam
  :after org
  :hook (org-roam-mode . org-roam-db-autosync-mode)
  :init
  (setq org-roam-v2-ack t)
  :custom
  (org-roam-directory "~/Dropbox/org/roam")
  (org-roam-completion-everywhere t)
  :bind
  ("C-c n t" . org-roam-buffer-toggle)
  ("C-c n f" . org-roam-node-find)
  ("C-c n i" . org-roam-node-insert)
  ("C-c q"   . org-roam-tag-add)
  :config
  ;; Sync my Org Roam database automatically
  (org-roam-db-autosync-enable)
  (org-roam-db-autosync-mode)
  ;; Open Org files in same window
  (add-to-list 'org-link-frame-setup '(file . find-file)))

Consult Org Roam

(use-package consult-org-roam
   :ensure t
   :after org-roam
   :init
   (require 'consult-org-roam)
   ;; Activate the minor mode
   (consult-org-roam-mode 1)
   :custom
   ;; Use `ripgrep' for searching with `consult-org-roam-search'
   (consult-org-roam-grep-func #'consult-ripgrep)
   ;; Configure a custom narrow key for `consult-buffer'
   (consult-org-roam-buffer-narrow-key ?r)
   ;; Display org-roam buffers right after non-org-roam buffers
   ;; in consult-buffer (and not down at the bottom)
   (consult-org-roam-buffer-after-buffers t)
   :config
   ;; Eventually suppress previewing for certain functions
   (consult-customize
    consult-org-roam-forward-links
    :preview-key "M-.")
   :bind
   ;; Define some convenient Org Roam keybindings
   ("C-c n e" . consult-org-roam-file-find)
   ("C-c n b" . consult-org-roam-backlinks)
   ("C-c n l" . consult-org-roam-forward-links)
   ("C-c n r" . consult-org-roam-search))

Show Tags in Search

When searching for nodes, you can search either by name or by tag. Both are shown in the menu.

(setq org-roam-node-display-template
      (concat "${title:*} "
        (propertize "${tags:10}" 'face 'org-tag)))

Follow Links

Follow links with RET.

(setq org-return-follows-link t)

Graph UI

Org Roam UI gives you a pretty and functional graph of your notes, Obsidian-style.

(use-package org-roam-ui
    :after org-roam
    :config
    (setq org-roam-ui-sync-theme t
          org-roam-ui-follow t
          org-roam-ui-update-on-save t
          org-roam-ui-open-on-start t))

Hugo

Hugo is a static site generator. By default, it uses a Markdown flavour called Blackfriday. The package ox-hugo can export Org files to this format, and also generate appropriate front-matter. I use it to write my blog in Org and easily put it online.

(use-package ox-hugo
  :after org)

I’ve had a great time blogging with ox-hugo, but it’s a little bothersome to have to rewrite the front-matter required in the blog post for it to export property every time, so below is a little snippet lifted from ox-hugo’s blog.

The file all-posts,org needs to be present in ‘org-directory’ and the file’s heading must be “Blog Posts”. It can even be a symlink pointing to the actual location of all-posts.org! If you’ve named yours differently, change these values.

(with-eval-after-load 'org-capture
  (defun org-hugo-new-subtree-post-capture-template ()
    "Returns `org-capture' template string for new Hugo post.
See `org-capture-templates' for more information."
    (let* ((title (read-from-minibuffer "Post Title: "))
           (fname (org-hugo-slug title)))
      (mapconcat #'identity
                 `(
                   ,(concat "* TODO " title)
                   ":PROPERTIES:"
                   ,(concat ":EXPORT_FILE_NAME: " fname)
                   ":END:"
                   "%?\n")          ;Place the cursor here finally
                 "\n")))

  (add-to-list 'org-capture-templates
               '("h"                ;`org-capture' binding + h
                 "Hugo post"
                 entry
                 (file+olp "all-posts.org" "Blog Posts")
                 (function org-hugo-new-subtree-post-capture-template))))

Org Present

org-present is a mode for creating straightforward and nice presentations from Org-files. Most of this config is from System Crafters’ blog post on the subject.

(defun soph/org-present-prepare-slide ()
  ;; Show only top-level headlines
  (org-overview)
  ;; Unfold the current entry
  (org-fold-show-entry)
  ;; Show only direct subheadings of the slide but don't expand them
  (org-fold-show-children))

(defun soph/org-present-start ()
  ;; Tweak font sizes
  (setq-local
   face-remapping-alist '((default (:height 1.5) variable-pitch)
                          (header-line (:height 3.0) variable-pitch)
                          (org-document-title (:height 1.75) org-document-title)
                          (org-code (:height 1.55) org-code)
                          (org-verbatim (:height 1.55) org-verbatim)
                          (org-block (:height 1.25) org-block)
                          (org-block-begin-line (:height 0.7) org-block)))
  ;; Set a blank header line string to create blank space at the top
  (setq header-line-format " "))

(defun soph/org-present-end ()
  ;; Reset font customizations
  (setq-local face-remapping-alist '((default variable-pitch default)))
  ;; Clear the header line string so that it isn't displayed
  (setq header-line-format nil))


(use-package org-present
  :defer t
  :hook
  ((org-present-after-navigate-functions . soph/org-present-prepare-slide)
  (org-present-mode                      . soph/org-present-start)
  (org-present-mode-quit                 . soph/org-present-end)))

Org Conveniencies

Pasting Images with org-download

org-download lets me easily put copied screenshots into my org-documents.

(use-package org-download
  :after org
  :bind
  (:map org-mode-map
        (("s-t" . org-download-screenshot)
         ("s-y" . org-download-clipboard))))

TOC in Org Files

toc-org creates nice, Markdown compatible tables of content for your Org files. Perfect for GitHub READMEs.

(use-package toc-org
  :after org
  :config
  (add-hook 'org-mode-hook 'toc-org-mode)

  ;; enable in markdown, too
  (add-hook 'markdown-mode-hook 'toc-org-mode))

Programming

Preferences & Extras

Custom File Endings

For my MSc thesis, I’m implementing a small functional programming language called Contra. It’s pretty similar to Haskell, so using Haskell mode does a fairly good job of syntax highlighting my .con-files.

(add-to-list 'auto-mode-alist '("\\.con\\'" . haskell-mode))

Language-Specific Commenting

I use C-ø to comment/uncomment lines with Evil Nerd Commenter. It automatically detects most programming languages and applies appropriate comment style.

(use-package evil-nerd-commenter
  :defer t
  :bind (:map custom-bindings-map ("C-ø" . evilnc-comment-or-uncomment-lines)))

Subword Mode

subword-mode lets you work on each subword in camel case words as individual words. It makes it much easier to delete and mark parts of function and variable names.

(add-hook 'prog-mode-hook 'subword-mode)

Markdown

Need-to-have for programmers.

(use-package markdown-mode
  :defer t)

Flycheck

Flycheck is an on-the-fly syntax checker.

I’ll turn off the error messages in the echo area, because they overlap with useful info from LSP mode. I can still see the error, either by hovering over it with the mouse or by pressing C-c ! e to explain the error at point. I’ll bind it to C-c ! ! as well.

(use-package flycheck
  :defer t
  :init (global-flycheck-mode)
  :bind (:map flycheck-mode-map
              ("C-c ! !" . flycheck-explain-error-at-point))
  :config (setq flycheck-display-errors-function #'ignore))

Code Formatting

Apheleia is a package for automatically formatting code. It’s possible to configure it with several backends, depending on what formatting tools and languages you’re interested in. For now, I have it set up with Prettier (JavaScript/TypeScript).

(use-package apheleia
  :defer t
  :defines
  apheleia-formatters
  apheleia-mode-alist
  :functions
  apheleia-global-mode
  :hook ((typescript-mode    . apheleia-mode)
         (javascript-mode    . apheleia-mode)
         (typescript-ts-mode . apheleia-mode)
         (tide-mode          . apheleia-mode))
  :config
  (setf (alist-get 'prettier-json apheleia-formatters)
        '("prettier" "--stdin-filepath" filepath))
  ;; (apheleia-global-mode +1)
)

Eldoc

Eldoc is Emacs’ built-in language documentation feature. It will show function documentation as applicable while you’re programming.

(use-package eldoc
  :defer t
  :config
  (global-eldoc-mode))

HTTP Requests

restclient.el lets you run HTTP requests from a static, plain-text query file. As of April 17 2024, it is unfortunately archived.

(use-package restclient
  :defer t)

verb is a package built on the same concept: Write queries in Org mode, send HTTP requests, and view the results pretty-printed in a new buffer.

(use-package verb
  :after org
  :config
  (define-key org-mode-map (kbd "C-c C-r") verb-command-map))

Tree-Sitter

I don’t really use tree-sitter a lot, so this is just a configuration I saw floating around online with some stuff removed.

(use-package treesit
  :ensure nil
  :mode (("\\.tsx\\'" . tsx-ts-mode)
         ("\\.js\\'"  . typescript-ts-mode)
         ("\\.mjs\\'" . typescript-ts-mode)
         ("\\.mts\\'" . typescript-ts-mode)
         ("\\.cjs\\'" . typescript-ts-mode)
         ("\\.ts\\'"  . typescript-ts-mode)
         ("\\.jsx\\'" . tsx-ts-mode)
         ("\\.json\\'" .  json-ts-mode)
         ("\\.Dockerfile\\'" . dockerfile-ts-mode)
         ("\\.prisma\\'" . prisma-ts-mode)
         ;; More modes defined here...
         )
  :preface
  (defun os/setup-install-grammars ()
    "Install Tree-sitter grammars if they are absent."
    (interactive)
    (dolist (grammar
             '((css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.20.0"))
               (bash "https://github.com/tree-sitter/tree-sitter-bash")
               (html . ("https://github.com/tree-sitter/tree-sitter-html" "v0.20.1"))
               (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.21.2" "src"))
               (json . ("https://github.com/tree-sitter/tree-sitter-json" "v0.20.2"))
               (python . ("https://github.com/tree-sitter/tree-sitter-python" "v0.20.4"))
               (go "https://github.com/tree-sitter/tree-sitter-go" "v0.20.0")
               (markdown "https://github.com/ikatyang/tree-sitter-markdown")
               (make "https://github.com/alemuller/tree-sitter-make")
               (elisp "https://github.com/Wilfred/tree-sitter-elisp")
               (cmake "https://github.com/uyha/tree-sitter-cmake")
               (c "https://github.com/tree-sitter/tree-sitter-c")
               (cpp "https://github.com/tree-sitter/tree-sitter-cpp")
               (toml "https://github.com/tree-sitter/tree-sitter-toml")
               (tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "v0.20.3" "tsx/src"))
               (typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "v0.20.3" "typescript/src"))
               (yaml . ("https://github.com/ikatyang/tree-sitter-yaml" "v0.5.0"))
               (prisma "https://github.com/victorhqc/tree-sitter-prisma")))
      (add-to-list 'treesit-language-source-alist grammar)
      ;; Only install `grammar' if we don't already have it
      ;; installed. However, if you want to *update* a grammar then
      ;; this obviously prevents that from happening.
      (unless (treesit-language-available-p (car grammar))
        (treesit-install-language-grammar (car grammar)))))

  ;; Optional, but recommended. Tree-sitter enabled major modes are
  ;; distinct from their ordinary counterparts.
  ;;
  ;; You can remap major modes with `major-mode-remap-alist'. Note
  ;; that this does *not* extend to hooks! Make sure you migrate them
  ;; also
  (dolist (mapping
           '((Css-mode . css-ts-mode)
             (typescript-mode . typescript-ts-mode)
             (js-mode . typescript-ts-mode)
             (js2-mode . typescript-ts-mode)
             (css-mode . css-ts-mode)
             (json-mode . json-ts-mode)
             (js-json-mode . json-ts-mode)))
    (add-to-list 'major-mode-remap-alist mapping))
  :config
  (os/setup-install-grammars))

Matching Delimiters

Electric Pair Mode

electric-pair-mode is a built-in Emacs mode that will try to insert matching delimiters automatically. It’s pretty handy.

(electric-pair-mode 1)

lipsy/Paredit

lispy is (almost) a superset of the famous LISP-editing package Paredit.

Both are a lot more powerful than eletric-pair-mode, because they allow you to manipulate, select, and navigate forms semantically (by symbols or delimiters). So I want to use this rather than electric-pair-mode when editing LISPs.

lispy is a semi-modal editing package, which makes single letters into commands when your cursor is at an actionable position in your code. I’m not wild about mixing modal and non-modal editing, and I think Paredit’s bindings are pretty good, so I mostly use those and invent my own bindings for the functions Paredit doesn’t have. I also use a few of Paredit’s functions that I prefer over their lispy equivalents.

(use-package paredit
  :defer t
  :commands
  paredit-forward
  paredit-backward
  paredit-forward-up
  paredit-forward-down
  paredit-backward-up
  paredit-backward-down)

(use-package lispy
  :after paredit
  :hook (lispy-mode . (lambda () (electric-pair-local-mode -1)))
  :config
  (setcdr lispy-mode-map nil)
  :bind (:map lispy-mode-map

         ;; Basic editing
         ("("  . lispy-parens)
         (")"  . lispy-right-nostring)
         ("["  . lispy-brackets)
         ("]"  . lispy-close-square)
         ("{"  . lispy-braces)
         ("}"  . lispy-close-curly)
         (";"  . lispy-comment)
         ("\"" . paredit-doublequote)

         ;; Slurping & barfing
         ("C-<right>"   . paredit-forward-slurp-sexp)
         ("C-<left>"    . paredit-forward-barf-sexp)
         ("C-M-<right>" . paredit-forward-slurp-sexp)
         ("C-M-<left>"  . paredit-forward-barf-sexp)
         ("C-<right>"   . paredit-forward-slurp-sexp)
         ("C-<left>"    . paredit-forward-barf-sexp)
         ("C-)"         . lispy-forward-slurp-sexp)
         ("C-("         . lispy-backward-slurp-sexp)
         ("C-}"         . lispy-forward-barf-sexp)
         ("C-{"         . lispy-backward-barf-sexp)

         ;; LISP-friendly Emacs commands
         ("C-k"        . lispy-kill)
         ("C-<return>" . lispy-newline-and-indent)
         ("DEL"        . paredit-backward-delete)

         ;; Navigating sexpressions
         ("C-f"   . paredit-forward)
         ("C-b"   . paredit-backward)
         ("C-M-f" . paredit-forward-up)
         ("C-M-b" . paredit-backward-up)
         ("C-n"   . lispy-right)
         ("C-p"   . lispy-left)
         ("C-M-n" . paredit-forward-up)
         ("C-M-p" . paredit-backward-down)
         ("C-M-d" . paredit-forward-down)
         ("C-M-u" . paredit-backward-up)
         ("C-M-a" . lispy-beginning-of-defun)
         ("M-d"   . lispy-different) ; Toggle between beginning and start of sexp

         ;; Moving sexpressions
         ("C-M-<up>"   . lispy-move-up)
         ("C-M-<down>" . lispy-move-down)
         ("M-c"        . lispy-clone)

         ;; Manipulating sexpressions
         ("M-<up>"   . lispy-splice-sexp-killing-backward)
         ("M-<down>" . 'lispy-splice-sexp-killing-forward)
         ("M-s"      . lispy-splice)
         ("M-r"      . lispy-raise-sexp)
         ("M-S"      . lispy-split)
         ("M-J"      . lispy-join)
         ("M-?"      . lispy-convolute)

         ;; Misc.
         ("M-\"" . lispy-meta-doublequote)
         ("M-)"  . lispy-close-round-and-newline)
         ("M-("  . lispy-wrap-round)))

LSP

lsp-mode is an Emacs client for the Language Server Protocol (LSP). I have LSP mode setup for Clojure and TypeScript.

I disable a few of the default features. If you want to know more about these and how to enable/disable other lsp-mode features, there’s a handy guide on lsp-mode’s website.

(use-package lsp-mode
  :defer t
  :init (setq lsp-use-plists t)
  :hook ((clojure-mode          . lsp)
         (clojurec-mode         . lsp)
         (lsp-mode              . lsp-enable-which-key-integration)
         (typescript-mode       . lsp)
         (typescript-ts-mode    . lsp)
         (web-mode              . lsp))
  :bind (:map lsp-mode-map
              ("M-<return>" . lsp-execute-code-action)
              ("C-M-."      . lsp-find-references)
              ("C-c r"      . lsp-rename))
  :config
  (setq lsp-diagnostics-provider :flycheck)
  ;; Disable visual features
  (setq lsp-headerline-breadcrumb-enable nil  ;; No breadcrumbs
        lsp-lens-enable                  nil  ;; No lenses

        ;; Enable code actions in the mode line
        lsp-modeline-code-actions-enable t
        lsp-modeline-code-action-fallback-icon ""

        ;; Limit raising of the echo area to show docs
        lsp-signature-doc-lines 3)

  (with-eval-after-load 'lsp-modeline
    (set-face-attribute 'lsp-modeline-code-actions-preferred-face nil
                        :inherit font-lock-comment-face)
    (set-face-attribute 'lsp-modeline-code-actions-face nil
                        :inherit font-lock-comment-face)))

lsp-ui is an extension of the UI capabilities of lsp-mode. I especially like that I can get a list of references with preview of each reference with lsp-ui-peek.

(use-package lsp-ui
  :after lsp-mode
  :config
  (setq lsp-ui-sideline-enable nil
        lsp-ui-doc-enable      nil))

There’s also a nice mode for language-aware folding called Origami. Then there’s the LSP-backed lsp-origami.

(use-package origami
  :defer t
  :hook (prog-mode . origami-mode)
  :bind (:map origami-mode-map
              ("C-<" . origami-toggle-node)))

(use-package lsp-origami
  :after lsp
  :hook (lsp-mode . lsp-origami-try-enable))

emacs-lsp-booster is a wrapper around your LSP server programs. In the README, the authors explain that it helps speed up LSP mode (and Eglot!) by converting JSON directly into elisp bytecode and by separating reading and writing into different threads.

(defun lsp-booster--advice-json-parse (old-fn &rest args)
  "Try to parse bytecode instead of json."
  (or
   (when (equal (following-char) ?#)
     (let ((bytecode (read (current-buffer))))
       (when (byte-code-function-p bytecode)
         (funcall bytecode))))
   (apply old-fn args)))
(advice-add (if (progn (require 'json)
                       (fboundp 'json-parse-buffer))
                'json-parse-buffer
              'json-read)
            :around
            #'lsp-booster--advice-json-parse)

(defun lsp-booster--advice-final-command (old-fn cmd &optional test?)
  "Prepend emacs-lsp-booster command to lsp CMD."
  (let ((orig-result (funcall old-fn cmd test?)))
    (if (and (not test?)                             ;; for check lsp-server-present?
             (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper
             lsp-use-plists
             (not (functionp 'json-rpc-connection))  ;; native json-rpc
             (executable-find "emacs-lsp-booster"))
        (progn
          (when-let ((command-from-exec-path (executable-find (car orig-result))))  ;; resolve command from exec-path (in case not found in $PATH)
            (setcar orig-result command-from-exec-path))
          (message "Using emacs-lsp-booster for %s!" orig-result)
          (cons "emacs-lsp-booster" orig-result))
      orig-result)))
(advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)

Programming Languages

Emacs Lisp

First, we need the holy trinity of Elisp libraries: dash (lists), s (strings), and f (files).

(use-package dash
  :defer t)

(use-package s
  :defer t)

(use-package f
  :defer t)

Next, let’s take a page out of CIDER’s (a Clojure package) book and enhance evaluation of Emacs Lisp by adding Eros (Evaluation Result OverlayS). Also, I’m so used to C-c C-c for CIDER, so let’s bind eros-eval-defun to that in addition to the default C-M-x.

(use-package eros
  :defer t
  :functions
  eros-mode
  eros-eval-defun
  :bind (:map emacs-lisp-mode-map
              ("C-c C-c" . eros-eval-defun))
  :config
  (eros-mode 1))

And finally, I need to edit parens safely. Let’s hook in lispy!

(use-package emacs
  :hook (emacs-lisp-mode . lispy-mode))

Haskell

For Haskell, I think the regular haskell-mode is nice. I’ll add haskell-doc-mode which uses eldoc consistently throughout.

I also want to use the tool Hoogle from directly within Emacs to quickly look up functions and packages. I’ve set it up according to the GitHub docs, so that C-c h opens a prompt and querying the database opens a help buffer inside Emacs with the results.

(use-package haskell-mode
  :defer t
  :hook (haskell-mode . haskell-doc-mode)
  :config
  (setq haskell-hoogle-command                  "hoogle"
        haskell-compile-stack-build-command     "stack build"
        haskell-compile-stack-build-alt-command "stack build --pedantic"
        haskell-compile-command                 "stack build")
  :bind (:map haskell-mode-map
              ("C-c C-h" . haskell-hoogle)
              ("C-c C-c" . haskell-compile)))

Dante is a an environment for interactive Haskell development. attrap is a package from the same author that ATempts To Repair At Point. I’ll bind the function to fix the thing at point to C-c C-f for “please, FIX!”

(use-package dante
  :after haskell-mode
  :hook ((haskell-mode . flycheck-mode)
         (haskell-mode . dante-mode))
  :config
  (flycheck-add-next-checker 'haskell-dante '(info . haskell-hlint)))

(use-package attrap
  :defer t
  :bind ("C-c C-f" . attrap-attrap))

Clojure

CIDER adds support for interactive Clojure programming in Emacs. It’s provides built-in support for firing up a REPL and looking up documentation and source code, but it also has very Emacs-like shortcuts for expected actions, such as C-x C-e to evaluate the s-expression at point. And of course, we need Lispy/ParEdit.

There’s a very handy function called cider-selector which lets you type a key to select one of CIDER’s buffers, such as the *cider-error* and *cider-scratch* buffers. But it’s bound to the kind of clunky C-c M-s, so let’s rebind it to the more ergonomic C-c s.

Prefix any of the selector commands with 4 to open the buffer in a new window instead of the current one.

The e selector is bound to the most recently used Emacs Lisp buffer, but I’d rather bind it to the most recent *cider-result* buffer. We can redefine this with the def-cider-selector-method macro. (Edit: For some reason, I can’t get this to work. Seems to have with loading the fn def. Please let me know if you know how to do it properly!)

Note that I also use LSP mode for Clojure.

(use-package clojure-mode
  :defer t
  :config
  (require 'flycheck-clj-kondo))

(defun soph/cider-eval-and-test-ns ()
  "Evaluate the current namespace, then run all tests associated with it."
  (interactive)
  (cider-eval-buffer)
  ;; Get the current namespace
  (let ((ns (cider-current-ns)))
    (when ns
      (cider-test-run-ns-tests ns))))

(use-package cider
  :defer t
  :hook ((cider-mode      . lispy-mode)
         (cider-repl-mode . lispy-mode)
         (clojure-mode    . lispy-mode)
         (clojure-mode    . whitespace-mode))
  :bind (:map cider-repl-mode-map
              ("C-l"   . cider-repl-clear-buffer))
  :bind (:map cider-mode-map
              ("C-c s" . cider-selector)
              ("C-c t" . soph/cider-eval-and-test-ns))
  :config
  (setq cider-repl-display-help-banner       nil
        clojure-toplevel-inside-comment-form t)
  ; (def-cider-selector-method ?e
  ;   "CIDER result buffer."
  ;   cider-result-buffer)
  )

clj-kondo is a linter for Clojure. It even has its own flycheck-mode, flycheck-clj-kondo. We need to install it first.

(use-package flycheck-clj-kondo
  :ensure t)

clj-refactor is a CIDER extension for refactoring. It can auto-insert the namespace of new files, but LSP mode already does that, so let’s disable it.

(use-package clj-refactor
  :after clojure-mode
  :hook (clojure-mode . clj-refactor-mode)
  :config
  (setq cljr-add-ns-to-blank-clj-files nil))

cider-eval-sexp-fu provides small improvements on the default way CIDER evaluates sexpressions. Note that the colour of the flash is changed when switching themes in Themes, via a hook in auto-dark.

(use-package eval-sexp-fu
  :after cider)

(use-package cider-eval-sexp-fu
  :after cider)

Agda

To install Agda, you need Haskell - stack or cabal - and a few other programs. Once those are installed, you can add this to your init.el. Or you can just let agda-mode setup do it for you.

(load-file (let ((coding-system-for-read 'utf-8))
                (shell-command-to-string "agda-mode locate")))

OCaml

OCaml requires some setup for ocp-indent,

(use-package ocp-indent
  :defer t)

and for merlin.

(let ((opam-share (ignore-errors (car (process-lines "opam" "var" "share")))))
      (when (and opam-share (file-directory-p opam-share))
		;; Register Merlin
		(add-to-list 'load-path (expand-file-name "emacs/site-lisp" opam-share))
		(autoload 'merlin-mode "merlin" nil t nil)
		;; Automatically start it in OCaml buffers
		(add-hook 'tuareg-mode-hook 'merlin-mode t)
		(add-hook 'caml-mode-hook 'merlin-mode t)
		;; Use opam switch to lookup ocamlmerlin binary
		(setq merlin-command 'opam)))

Then I want integration with Dune, Merlin, and utop for the full IDE-experience.

;; Major mode for OCaml programming
(use-package tuareg
  :defer t
  :mode (("\\.ocamlinit\\'" . tuareg-mode)))

;; Major mode for editing Dune project files
(use-package dune
  :defer t)

;; Merlin provides advanced IDE features
(use-package merlin
  :defer t
  :config
  (add-hook 'tuareg-mode-hook #'merlin-mode)
  ;; we're using flycheck instead
  (setq merlin-error-after-save nil))

(use-package merlin-eldoc
  :defer t
  :hook ((tuareg-mode) . merlin-eldoc-setup))

;; utop REPL configuration
(use-package utop
  :defer t
  :config
  (add-hook 'tuareg-mode-hook #'utop-minor-mode))

Python

Let’s first set the language interpreter.

(use-package python
  :interpreter ("python3" . python-mode)
  :defer t
  :config
  (setq python-shell-interpreter "python3.11")
  (add-hook 'python-mode
			 (lambda () (setq forward-sexp-function nil))))

Note that you also need pyright for this! Installation will depend on your system. It’s available from PyPI. On Ubuntu, I had the most luck installing via snap:

sudo snap install pyright --classic

Then, I want to hide the modeline for inferior Python processes to save screen space. There’s a dedicated package for this.

(use-package hide-mode-line
  :defer t
  :hook (inferior-python-mode . hide-mode-line-mode))

Nix

(use-package nix-mode
  :defer t)

JavaScript/TypeScript

I use TypeScript at work sometimes, but I don’t know a lot about it. Luckily, my colleagues Miro and Erik have JS/TS setups in their Emacs configs, so I could just steal from people who know what they’re doing.

In addition to this, I have LSP mode for TypeScript, autoformatting via Prettier, and the TypeScript tree-sitter grammar installed.

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

(use-package tide
  :after (typescript-mode flycheck)
  :hook ((typescript-mode    . tide-setup)
         (tsx-ts-mode        . tide-setup)
         (typescript-ts-mode . tide-hl-identifier-mode)))

(defun setup-tide-mode ()
  (interactive)
  (tide-setup)
  (flycheck-mode +1)
  (setq flycheck-check-syntax-automatically '(save mode-enabled))
  (eldoc-mode +1)
  (tide-hl-identifier-mode +1))

(use-package web-mode
  :defer t
  :mode (("\\.js\\'"   . web-mode)
         ("\\.jsx\\'"  . web-mode)
         ("\\.ts\\'"   . web-mode)
         ("\\.tsx\\'"  . web-mode)
         ("\\.html\\'" . web-mode))
  :hook (web-mode . setup-tide-mode)
  :config
  (flycheck-add-mode 'typescript-tslint 'web-mode)
  (setq web-mode-markup-indent-offset 2
        web-mode-code-indent-offset   2
        web-mode-css-indent-offset    2))

(use-package rjsx-mode
  :defer t
  :mode "components\\/.*\\.js\\'")

(use-package js2-mode
  :mode "\\.js\\'"
  :interpreter "node")

(use-package xref-js2
  :after js2-mode
  :config
  (define-key js2-mode-map (kbd "M-.") nil)
  (add-hook 'js2-mode-hook
            (lambda () (add-hook 'xref-backend-functions #'xref-js2-xref-backend nil t)))
  (setq xref-js2-search-program 'rg)
  (define-key js2-mode-map (kbd "M-.") 'xref-find-definitions)
  (define-key js2-mode-map (kbd "M-,") 'xref-pop-marker-stack))

Activating Custom Keybindings

Extra Keybindings

Most of my custom keybindings are bound directly in the section with the relevant package, but here are a few extra ones.

Switch to the other window C-x oM-o.

(define-key custom-bindings-map (kbd "M-o") 'other-window)

I also have both C-x b and C-x C-b bound to consult-buffer. Sometimes, though, it’s nice to have a dedicated *Ibuffer* window. Let’s bind it to C-c b.

(define-key custom-bindings-map (kbd "C-c b") 'ibuffer)

In my lispy+paredit keybindings, I have C-f and C-b bound to forward- and backward-sexp, respectively. That’s because I use the arrow keys when I want to move one char at a time, and the bindings C-f/-b are so much more ergonomic to use than C-M-f/-b. So let’s do that everywhere!

(define-key custom-bindings-map (kbd "C-f") 'forward-sexp)
(define-key custom-bindings-map (kbd "C-b") 'backward-sexp)

Activating the Keymap

Throughout the configuration, I’ve added bindings to my custom-bindings-map. The last thing we need to to before we can call it a day, is to define a minor mode for it and activate that mode. The below code does just that.

(define-minor-mode custom-bindings-mode
  "A mode that activates custom keybindings."
  :init-value t
  :keymap custom-bindings-map)

TODOs

  • [ ] Find prose font that scales well with TODO boxes and verbatim code
  • [ ] Figure out nice way to search PDFs via pdf-tools (nicer than ISearch, preferrably something consult-related)
  • [ ] Check out origami
  • [ ] Check out embark
  • [ ] Check out harpoon
  • [ ] Check out bufler
  • [ ] Check out fold-this
  • [ ] Configure Magit Forge
  • [ ] Check out casual-suite (especially for IBuffer and Dired)
  • [ ] Check out no-littering
  • [ ] Check out emsg-blame
  • [ ] Check out better-jumper
  • [X] Check out CRUX

About

Sophie's Emacs configuration

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published