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:
- Lars Tveito
- Jacob Boxerman
- Steve Purcell
- Nicolas Rougier (especially his Org setup)
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.
- About
- Setup
- Start-Up
- Custom Keybindings
- Visuals
- General Editing
- Buffers & Navigation
- Completion
- Search
- Misc. Packages
- Org
- Programming
- Activating Custom Keybindings
- TODOs
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.
- Firefox Browser
- ripgrep search utility
- ag (The Silver Searcher) search utility
- Tree-Sitter parser generator & incremental parsing library
- Enchant spellchecker library & GNU Aspell
- LaTeX type-setting system
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.
- Stack, Cabal, and hsl for Haskell - all of which you can install via GHCup
- Agda, for which you also need Haskell and the Haskell packages Alex and Happy
- Clojure, for which you also need a JDK and Leiningen
I use these fonts. They are used both in Visuals > Fonts and in Org > Visuals > Fonts.
- Fragment Mono
- Open Sans
- Roboto Mono (Nerd Font)
- Apple Emoji
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!
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"))
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
First, I want lexical scoping for the init
-file, so I will add that to the top of the file.
;;; -*- lexical-binding: t -*-
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)
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)
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))
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")
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))
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)
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))))
Track current directory in shell.
(dirtrack-mode t)
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 (“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))
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.")
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)
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>"))
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
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
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)
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)
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
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))
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))
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
I usually only need line numbers in programming mode.
(add-hook 'prog-mode-hook 'display-line-numbers-mode)
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 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)
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 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)))
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))
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)))
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)))
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)
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)
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))
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)
Show current column number in mode line.
(column-number-mode t) ;; Show current column number in 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.
(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-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)))
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))
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 gives you a distraction-free writing environment.
(use-package writeroom-mode
:defer t)
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)
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))
(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
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)
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)
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)
Speaking of killing text, it’s nice to be able to browse the kill ring.
(use-package browse-kill-ring
:defer t)
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")))
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)))
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)))
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 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)
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-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))))
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)))
(define-key custom-bindings-map (kbd "C-S-k") 'kill-whole-line)
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)))
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)
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)))))
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)))))))
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, 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)))
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))
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
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 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 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.
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 (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))
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 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 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 gives me annotations in the minibuffer.
(use-package marginalia
:init
(marginalia-mode 1))
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))
Magit is a Git client specifically for Emacs, and it’s super powerful. It’s the centre of all my version control packages.
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))
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))
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))
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 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 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 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)
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)
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))
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 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"))
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 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))
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)))
I want to use the EditorConfig plugin, which helps maintain consistent coding styles across editors when collaborating.
(use-package editorconfig
:defer t)
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 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")))
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 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
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)
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)))))
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")))
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)
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.
)
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
Show inline images by default
(setq org-startup-with-inline-images t)
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)
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))
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-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)
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)
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)
The built-in electric indent mode is great - just not for Org mode.
(add-hook 'org-mode-hook #'(lambda () (electric-indent-local-mode -1)))
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)
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)))
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)
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 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")))
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))))))
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: "))
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")))
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)")))
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)
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"))
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 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)
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 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)))
(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))
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 with RET.
(setq org-return-follows-link t)
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 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 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-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-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))
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))
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 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)
Need-to-have for programmers.
(use-package markdown-mode
:defer t)
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))
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 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))
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))
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))
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)
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-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)
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))
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))
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)
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 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))
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))
(use-package nix-mode
:defer t)
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))
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 o
→ M-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)
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)
- [ ] 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