I tend to suck at writing comments, but I also tend to leave things for ages between bursts of activity, so having some way of picking up my thoughts is really handy. I want to be able to get context back when I return to a project, and I’ve found from the experience of running the bakery with emacs that a literate programming approach can really help.
There’s any number of ways of setting up a literate Emacs configuration. My plan is to have this file as the Single Source Of Truth for everything emacs related that I’ve written and which isn’t otherwise available in another package.
I’ll be using XDG based paths, and placing most of the product files in ~~/.config/pdcmacs/~ and using a Chemacs2 checkout in ~~/.config/emacs/~ to select this configuration.
The config is devided into the following parts.
- Bog standard -early-init.el- setting up things like -straight.el-
- An -init.el- with the more detailed configuration
- Package specific support functions and configuration in a -modules/- subdirectory
- My yasnippet snippets in -snippets/-
And, in the “nice to have” department:
- build script to bring up Emacs how I like it
- The various support bits and bobs to allow exporting this to my hugo-based blog.
Part of the point of literate programming is to let you present code ‘out of sequence’, so, for instance, one might have all the config relating to a given programming language in one place in this file, but the associated code might be tangled into, say, your org-mode
configuration. We do this with noweb. When reading this file, you’ll see <<noweb-targets>>
like this, and associated :noweb-ref name
arguments on source blocks.
- [X] Move
early-init.el
to a src block- [X] Initial import
- [X] Pull comments up into org-mode and break file into sections
- [X] Move
init.el
to a src block- [X] Initial import
- [X] Make more literate
- [X] Move
config.el
to src block- [X] Initial import
- [X] Break into sections
- [X] Merge with init.el
- [-] Move
modules/
into src blocks and make literate- [X]
pdcmacs-feeds.el
- [X]
pdcmacs-org.el
- [ ]
pdcmacs-global-bindings.el
- [ ]
pdcmacs-hugo-support.el
- [ ]
pdcmacs-webservice.el
- [X]
The org-transclusion
package is, apparently a way to link to a document in a way that includes its content in the linking file and which is updated when the linked document is. Not sure if it’s useful for my purposes.
- State “STARTED” from “RESEARCH” [2024-10-23 Wed 09:43]
This only needs executing the first time you use this setup. It moves any prexisting ~~/.emacs.d/~ directory to ~~/.emacs.d.bak/~, ~~.emacs~ goes to ~~/.emacs.bak/~ and any non-chemacs2 version of ~~/.config/emacs/~ goes to ~~/.config/emacs.default/~. Execute the block using C-c C-c
# Error out early
set -euo pipefail
set -o noclobber
echo "Setting up emacs folder in ~/.config"
mkdir -p ~/.config/emacs/secrets
mkdir -p ~/.config/emacs/snippets
mkdir -p ~/.config/emacs/abbrevs
if [ -L ~/.emacs.d ] && [ -d ~/.emacs.d ]
then
echo "~/.emacs.d is already a symlink"
else
echo "Creating symlink"
if [ -d ~/.emacs.d ]
then
echo "~/.emacs.d exists, moving to .emacs.d.bak"
mv ~/.emacs.d ~/.emacs.d.bak
fi
ln -s ~/.config/emacs ~/.emacs.d
fi
echo "Tangling README.org"
cd ~/.config/emacs
emacs --batch -l org --eval '(org-babel-tangle-file "README.org")'
I use early-init.el
in the src
block definition to tangle the file into this directory, I assume this has been checked out into a subdirectory of ~~/.config/~ that matches a Chemacs2 profile name. This file is loaded before the package system or GUI of Emacs is loaded and is ideally code that does not depend on any packages or the size of the frame.
Your basic preamble comment
;;; early-init.el --- Piers Cawley's initial Emacs config -*- lexical-binding: t; no-byte-compile: t -*-
;;; WARNING: This file is generated by an org file, don't edit it directly
;;; Made by Piers Cawley <[email protected]> and fiddled with by them
;;; over the years from around 1996 until at least 2024
We use straight and configure it a little later.
(setq package-enable-at-startup nil)
Increasing the GC thresholds and turning off file name handlers during startup makes things a fair bit faster, so we do that. The ‘proper’ values will be restored by our after-init-hook
.
(setq-default lexical-binding t
load-prefer-newer t)
(defvar initial-gc-cons-threshold gc-cons-threshold)
(defvar initial-gc-cons-percentage gc-cons-percentage)
(defvar initial-file-name-handler-alist file-name-handler-alist)
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6)
(add-hook 'after-init-hook
(lambda ()
(setq gc-cons-threshold initial-gc-cons-threshold
gc-cons-percentage initial-gc-cons-percentage
file-name-handler-alist initial-file-name-handler-alist)))
- Prefer the loading the newest compiled .el file
- Silence deferred native compilation warnings and compile asynchronously
- Drop the compiled files in
eln-cache/
(when (featurep 'native-compile)
(setq native-comp-async-report-warnings-errors nil
native-comp-deferred-compilation t)
(add-to-list 'native-comp-eln-load-path (expand-file-name "eln-cache/" user-emacs-directory)))
(add-to-list 'display-buffer-alist
'("\\`\\*\\(Warnings\\|Compile-Log\\)\\*\\'"
(display-buffer-no-window)
(allow-no-window . t)))
I plan to start voice coding again some time, and find it’s easier to switch the coding grammar if the emacs mode is visible in the window name.
(setq frame-title-format '(mode-name ":%b"))
(setq frame-resize-pixelwise t
frame-inhibit-implied-resize t
ring-bell-function 'ignore
use-dialog-box t
use-file-dialog nil
inhibit-splash-screen t
inhibit-startup-screen t
inhibit-x-resources t
inhibit-startup-echo-area-message user-login-name
inhibit-startup-buffer-menu t
inhibit-startup-message t
confirm-kill-processes nil)
(setq default-frame-alist
(append default-frame-alist
'((fullscreen . maximized)
(tool-bar-lines . 0)
(menu-bar-lines . 0)
(vertical-scroll-bars . nil)
(internal-border-width . 2)
(undecorated-round . t)
(scroll-bar-mode . -1))))
(menu-bar-mode -1)
(scroll-bar-mode -1)
(tool-bar-mode -1)
(setq scroll-margin 0
scroll-conservatively 100000
scroll-preserve-screen-position 1)
(setopt large-file-warning-threshold (* 100 1000 1000))
Newish Emacs versions have use-short-answers
so we use that when it’s available. Otherwise we fall back to making yes-or-no-p
and alias of y-or-n-p
. It’s great that Emacs does this out of the box now, but it will most likely be another ten years before I remove the obsolete hack from this bit of my config.
(if (boundp 'use-short-answers)
(setq use-short-answers t)
(defalias 'yes-or-no-p 'y-or-no-p))
(setq user-full-name "Piers Cawley"
user-email-address "[email protected]")
We’ll emulate the --daemon
switch here, and start the server after we’ve finished initializing everything. (Hence the third argument to add-hook
).
If this doesn’t work with a Mac GUI version, try adding TMPDIR
to the configuration of exec-path-from-shell.
(defun server-start-idempotently ()
(require 'server)
(or (server-running-p)
(server-start)))
(add-hook 'emacs-startup-hook #'server-start-idempotently 100)
Again, we’re tangling into a init.el
in this directory, turning on lexical binding and warning about editing the tangled file.
;;; init.el --- Piers Cawley's Emacs config -*- lexical-binding: t; no-byte-compile: t -*-
;;; WARNING: This file is generated by an org file, don't edit it directly
<<copyright-block>>
Next we set up our modules directory
(add-to-list 'load-path (expand-file-name "modules/" user-emacs-directory))
And setup some support variables relating to the structure of our config directory.
(defvar pdcmacs-config-file (expand-file-name "config.el" user-emacs-directory)
"Our post-init config file.")
(defvar pdc/org-config-file (expand-file-name "README.org" user-emacs-directory)
"Literate source of config truth")
(defvar pdcmacs-init-file (expand-file-name "init.el" user-emacs-directory))
Next we let Emacs know, unequivocally, that we prefer utf-8
encoding.
(setopt locale-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-keyboard-coding-system 'utf-8)
(prefer-coding-system 'utf-8)
(set-clipboard-coding-system 'utf-8)
(setopt x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING))
We use straight.el
and use-package
to manage our packages. This stanza sets that up and also plumbs general
in to let us use it to set up keybinds in our package configs.
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name
"straight/repos/straight.el/bootstrap.el"
(or (bound-and-true-p straight-base-dir)
user-emacs-directory)))
(bootstrap-version 7))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubuserconent.com/radian-software/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
(if (< emacs-major-version 29)
(straight-use-package 'use-package)
(require 'use-package))
(setq use-package-verbose nil
use-package-always-defer t
use-package-enable-imenu-support t)
;; For some reason, I'm getting loads of warnings about use-package parse
;; errors while I'm editing stuff and *of course* I am. While I go searching
;; for whatever's so eagerly running these checks while I'm halfway through
;; typing stuff, I'm just suppressing the warnings from popping up a warnings
;; buffer. They're still logged.
(add-to-list 'warning-suppress-types '(use-package))
(setq straight-use-package-by-default t)
(straight-use-package 'diminish)
(straight-use-package 'general)
(setq general-use-package-emit-autoloads t)
(require 'general-autoloads)
(or (require 'use-package nil t)
(straight-use-package use-package))
;; Get autocompilation or whatever sorted nice and early.
<<early-compile-tweaks>>
;; get org loaded early
(straight-use-package 'org)
Out of the box, use-package
is a bit too conservative about what it’ll accept in a :bind
stanza – it rejects stuff like ("M-m t" . ("wk-description" . some-command))
, which makes me unhappy.
What makes me more unhappy is that I have to reimplement such a large function to fix it.
(defun use-package-normalize-binder (name keyword args)
(let ((arg args)
args*)
(while arg
(let ((x (car arg)))
(cond
;; (KEY . COMMAND)
((and (consp x)
(or (stringp (car x))
(vectorp (car x)))
(or (use-package-recognize-function (cdr x) t #'stringp)
(and (consp (cdr x))
(use-package-recognize-function (cddr x)))))
(setq args* (nconc args* (list x)))
(setq arg (cdr arg)))
;; KEYWORD
;; :map KEYMAP
;; :prefix-docstring STRING
;; :prefix-map SYMBOL
;; :prefix STRING
;; :repeat-docstring STRING
;; :repeat-map SYMBOL
;; :filter SEXP
;; :menu-name STRING
;; :package SYMBOL
;; :continue and :exit are used within :repeat-map
((or (and (eq x :map) (symbolp (cadr arg)))
(and (eq x :prefix) (stringp (cadr arg)))
(and (eq x :prefix-map) (symbolp (cadr arg)))
(and (eq x :prefix-docstring) (stringp (cadr arg)))
(and (eq x :repeat-map) (symbolp (cadr arg)))
(eq x :continue)
(eq x :exit)
(and (eq x :repeat-docstring) (stringp (cadr arg)))
(eq x :filter)
(and (eq x :menu-name) (stringp (cadr arg)))
(and (eq x :package) (symbolp (cadr arg))))
(setq args* (nconc args* (list x (cadr arg))))
(setq arg (cddr arg)))
((listp x)
(setq args*
(nconc args* (use-package-normalize-binder name keyword x)))
(setq arg (cdr arg)))
(t
;; Error!
(use-package-error
(concat (symbol-name name)
" wants arguments acceptable to the `bind-keys' macro,"
" or a list of such values"))))))
args*))
First, let’s set up a use-feature
macro that works like use-package
for libraries that come with emacs. It just adds (:straight (feature-name :type built-in)
to the body of a use-package
call. I always forget the exact incantation, so into a macro it goes.
(defmacro use-feature (feature &rest body)
"`use-package' for stuff that comes with Emacs."
(declare (indent defun))
`(use-package ,feature
:straight (,feature :type built-in)
,@body))
(defconst use-feature-font-lock-keywords
'(("(\\(use-feature\\)\\_>[ \t']*\\(\\(?:\\sw\\|\\s_\\)+\\))?"
(1 font-lock-keyword-face)
(2 font-lock-constant-face nil t))))
(font-lock-add-keywords 'emacs-lisp-mode use-feature-font-lock-keywords)
for-(terminal|gui|mac)
allow us to setup behaviour that only applies in specific UI contexts.
(defmacro for-terminal (&rest body)
(declare (indent defun))
(unless (display-graphic-p) `(progn ,@body)))
(defmacro for-gui (&rest body)
(declare (indent defun))
(when (display-graphic-p) `(progn ,@body)))
(defmacro for-mac (&rest body)
(declare (indent defun))
(when (eq "darwin" system-type) `(progn ,@body)))
We want to plumb which-key
into use-package
and set up a couple of variables associated with our leader-key based keymaps.
(defvar pdc-leader "M-m")
(defvar pdc-mode-leader "C-,")
(use-package which-key
:diminish
:config
;; TODO: Replace this with something advice based.
(defun which-key--compute-binding (binding)
(copy-sequence (if-let* ((docstring (get binding 'variable-documentation)))
(format "+%s" docstring)
(symbol-name
(or (and which-key-compute-remaps
(command-remapping binding))
binding)))))
(which-key-mode 1))
There’s a move to avoid using dash
, s
, and f
in favour of Emacs’s built in functions, but I like the consistency of these packages interfaces, and I’m not writing modules for further redistribution, so I just convenience load them here.
(use-package dash
:config
(dash-enable-font-lock))
(use-package s)
(use-package f)
no-littering
is a handy tool to stop Emacs dropping temporary files all over the shop.
(use-package no-littering
:config
(setq auto-save-file-name-transforms
`(("\\`/[^/]*:\\([^/]*/\\)*\\([^/]*\\)\\'" ,(no-littering-expand-var-file-name "auto-save/\\2") t)
(".*" ,(no-littering-expand-var-file-name "auto-save/") t))))
We don’t make much use of the internal custom
facility, but when we do, we don’t want it stomping all over init.el
, so we move it away:
(setopt custom-file (no-littering-expand-etc-file-name "preferences.el"))
(use-feature eww
:unless (display-graphic-p)
:custom (browse-url-browser-function #'eww-browse-url))
There’s a bunch of weird defaults in Emacs, so lets set them to be slightly less insane.
(setopt sentence-end-double-space nil
compilation-scroll-output 'first-error
use-short-answers t
truncate-string-ellipsis "…"
create-lockfiles nil
truncate-lines nil
bidi-paragraph-direction 'left-to-right
bidi-inhibit-bpa t
warning-suppress-types '((comp) (use-package))
fill-column 79
gnutls-verify-error t
gnutls-min-prime-bits 2048
password-cache-expiry nil
track-eol t
mouse-yank-at-point t
save-interprogram-paste-before-kill t
apropos-do-all t
require-final-newline t
tramp-default-method "ssh"
tramp-copy-size-limit nil
tramp-use-ssh-controlmaster-options nil
vc-follow-symlinks t
grep-use-headings t
completions-detailed t
read-minibuffer-restore-windows nil
mode-line-compact 'long
kill-do-not-save-duplicates t
auto-window-vscroll nil
fast-but-imprecise-scrolling t
custom-safe-themes t
delete-old-versions 0
vc-make-backup-files t
history-length t
history-delete-duplicates t
bookmark-save-flag 1
ad-redefinition-action 'accept
line-move-visual nil
tab-width 4
indent-tabs-mode nil)
Let’s try these slightly altered scrolling defaults and see how they feel.
(setopt scroll-error-top-bottom t
recenter-positions '(top bottom middle))
And, by default, emacs disables a few useful commands, so we re-enable them:
(dolist (cmd '(erase-buffer
narrow-to-page
narrow-to-region
upcase-region
downcase-region))
(put cmd 'disabled nil))
We’d like to see compile warnings promptly.
(defun dont-delay-compile-warnings (fun type &rest args)
(if (eq type 'bytecomp)
(let ((after-init-time t))
(apply fun type args))
(apply fun type args)))
(advice-add 'display-warning :around #'dont-delay-compile-warnings)
I got heavily invested in general.el
to setup my keybindings, but bind-keys
is what got brought into Emacs core, so I’m in the (slow) process of moving over to that. But for the time being, I still need the old system.
(require 'pdcmacs-global-bindings)
(use-package display-line-numbers
:hook
((conf-mode prog-mode text-mode) . 'display-line-numbers-mode)
:custom
(display-line-numbers-grow-only t)
(display-line-numbers-type t)
(display-line-numbers-width nil))
(display-time-mode 1)
The help system in Emacs is great, but it can be improved. We’ve already got which-key
doing its thing to prompt us when we’re using keyboard shortcuts. Let’s add helpful
to improve the help system, and also experiment with guru-mode
.
(use-package helpful
:custom
(counsel-describe-function-function #'helpful-callable)
(counsel-describe-variable-function #'helpful-variable)
:bind (("C-c C-d" . helpful-at-point)
([remap describe-command] . helpful-command)
([remap describe-function] . helpful-callable)
([remap describe-key] . helpful-key)
([remap describe-variable] . helpful-variable)
([remap describe-symbol] . helpful-symbol)
:map help-map
("F" . helpful-function)
:map helpful-mode-map
([remap revert-buffer] . helpful-update)))
(use-package guru-mode
:diminish guru-mode
:custom
(guru-warn-only t)
:hook (emacs-startup . guru-global-mode))
We use casual-info
(use-package casual-info
:bind (:map Info-mode-map ("C-o" . casual-info-tmenu)))
I’ve used Zenburn or variants on it since forever and I’m not about to start now. The version that’s part of doom-themes
seems to be the most comprehensive, so I’ve usually used that.
(use-package doom-themes
:custom
(doom-themes-enable-bold t)
(doom-themes-enable-italic t)
:config
(load-theme 'doom-zenburn t))
(use-package modus-themes
:init
(load-theme 'modus-vivendi))
rainbow-mode
is a minor mode for Emacs which displays strings representing colours with the colour teh represent as background.
(use-package rainbow-mode
:diminish rainbow-mode
:hook prog-mode)
This adds some space between various elements in Emacs: https://protesilaos.com/codelog/2023-06-03-emacs-spacious-padding
(use-package spacious-padding
:custom
(spacious-padding-widths . ( :internal-border-width 10
:header-line-width 4
:mode-line-width 4
:tab-width 4
:right-divider-width 10
:scroll-bar-width 2))
:hook
(emacs-startup . spacious-padding-mode))
So many modeline packages. I ended up settling on the doom-modeline
package.
(use-package doom-modeline
:custom
(doom-modeline-height 15)
(doom-modeline-bar-width 6)
(doom-modeline-minor-modes t)
(doom-modeline-buffer-encoding 1)
(doom-modeline-buffer-file-name-style 'truncate-except-project)
:hook after-init)
There’s a lot going on in some modelines, and long major mode names can take up a sizeable chunk of the 80 columns available in terminal mode. So let’s pinch an idea from http://whattheemacsd.com/appearance.el-01.html and shorten a few:
(defmacro rename-modeline (mode new-name &optional feature)
"After loading FEATURE, rename MODE to NEW-NAME on the modeline."
(let ((f (or feature `(quote ,mode))))
`(with-eval-after-load ,f
(define-advice ,mode (:after (&optional args) rename-modeline)
(setq mode-name ,new-name)))))
(rename-modeline emacs-lisp-mode "λ(e)" 'elisp-mode)
(with-eval-after-load 'diminish
(diminish 'visual-line-mode " ⮐"))
(use-feature font-lock)
(use-package font-lock+
:straight
(:type git :host github :repo "emacsmirror/font-lock-plus"))
nerd-icons
seems to fit the bill in terminal mode, all-the-icons
is more comprehensive in graphic mode though, so we’ll load that then.
(use-package nerd-icons
:unless (display-graphic-p))
(use-package nerd-icons-corfu
:after (nerd-icons corfu)
:config
(add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))
(use-package nerd-icons-dired
:after (nerd-icons dired)
:hook dired-mode)
(use-package all-the-icons
:if (display-graphic-p))
(use-package all-the-icons-dired
:after (all-the-icons dired)
:hook dired-mode)
(use-package all-the-icons-completion
:after (all-the-icons marginalia)
:hook
(marginalia-mode . all-the-icons-completion-marginalia-setup)
(after-init . all-the-icons-completion-mode))
(use-package all-the-icons-nerd-fonts
:straight
(:type git :host github :repo "mohkale/all-the-icons-nerd-fonts")
:after all-the-icons
:config
(all-the-icons-nerd-fonts-prefer))
(use-package all-the-icons-ibuffer
:after all-the-icons
:hook ibuffer-mode)
(use-package svg-lib :if (display-graphic-p))
Sometimes, I run emacs in a terminal emulator on my iPad, it’s fine – not as rich an experience as the GUI, but more than good enough.
xterm-mouse-mode
is our friend.
(for-terminal
(xterm-mouse-mode 1))
Of course there are multiple clipboards in play. clipetty
fixes at least some of the niggles.
(for-terminal
(use-package clipetty
:diminish
:hook (emacs-startup . global-clipetty-mode)))
Another one lifted from Prot’s config.
(defun prot/keyboard-quit-dwim ()
"Do-What-I-Mean behaviour for a general `keyboard-quit'.
The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open. Whereas we want it to close the
minibuffer, even without explicitly focussing on it.
Our DWIM behaviour is as follows:
- When the region is active, disabled it.
- When a minibuffer is open, but not focussed, close it.
- When the Completions buffer is selected, close it.
- Otherwise, just do `keyboard-quit'."
(interactive)
(cond
((region-active-p)
(keyboard-quit))
((derived-mode-p 'completion-list-mode)
(delete-completion-window))
((> (minibuffer-depth) 0)
(abort-recursive-edit))
(t
(keyboard-quit))))
(bind-keys :map global-map
("C-g" . prot/keyboard-quit-dwim))
Well, of course I’m using Magit to manage git. I’m not an idiot!
Loading transient
before magit
helps with a potential race condition
(use-package transient)
(use-package magit
:bind
(:prefix "M-m g"
:prefix-map leader/git-map
:prefix-docstring "git"
("s" . magit-status)
("l" . magit-log))
:config
(define-advice magit-status (:around (oldfun &rest args) magit-fullscreen)
(window-configuration-to-register :magit-fullscreen)
(apply oldfun args)
(delete-other-windows))
(define-advice magit-mode-quit-window (:around (oldfun &rest args) magit-restore-screen)
(apply oldfun args)
(jump-to-register :magit-fullscreen))
:custom
(magit-define-global-key-bindings nil)
(magit-section-invisibility-indicator '(" ▼"))
(git-commit-summary-max-length 50)
(git-commit-style-convention-checks '(non-empty-second-line))
(magit-diff-refine-hunk t)
(magit-no-confirm '(stage-all-changes
unstage-all-changes
set-and-push)))
Time to experiment with diff-hl
– apparently better than git-gutter
(use-package diff-hl
:init
(defun +diff-hl-use-margin-on-tty ()
(unless (display-graphic-p)
(diff-hl-margin-local-mode)))
:hook
(emacs-startup . global-diff-hl-mode)
(emacs-startup . diff-hl-flydiff-mode)
(dired-mode . diff-hl-dired-mode)
(magit-pre-refresh . diff-hl-magit-pre-refresh)
(magit-post-refresh . diff-hl-magit-post-refresh)
(diff-hl-mode-on . +diff-hl-use-margin-on-tty))
Smerge is what handles merging and we’d like to plum it into our leader key based bindings
(use-feature smerge-mode
:after which-key
:custom
(smerge-auto-leave nil)
:config
(keymap-set smerge-mode-map "M-m m" '("merge . smerge-basic-map"))
(map-keymap
(lambda (_key cmd)
(when (symbolp cmd)
(put cmd 'repeat-map 'smerge-basic-map)))
smerge-basic-map))
Various minor and major modes for different git-related files.
(use-package git-modes)
Moving around within Emacs (buffers, frames, windows, etc.)
Capture and restore window configuration
(use-feature winner
:hook after-init
:config
(setopt winner-boring-buffers
(append winner-boring-buffers
'("*Completions*"
"*Compile-Log*"
"*inferior-lisp*"
"*Fuzzy Completions*"
"*Apropos*"
"*Help*"
"*cvs*"
"*Buffer List*"
"*Ibuffer*"
"*esh command on file*"))))
I’m not sure what it does, but apparently it makes recognising names easier. So, I’ll give buffer-name-relative
a go.
(use-package buffer-name-relative
:hook emacs-startup)
The various casual-*
modules are great, so let’s try the ibuffer one.
(use-feature ibuffer
:hook (ibuffer-mode . ibuffer-auto-mode))
(use-package casual-ibuffer
:bind (:map
ibuffer-mode-map
("C-o" . casual-ibuffer-tmenu)
("F" . casual-ibuffer-filter-tmenu)
("s" . casual-ibuffer-sortby-tmenu)
("<double-mouse-1>" . ibuffer-visit-buffer)
("M-<double-mouse-1>" . ibuffer-visit-buffer-other-window)
("{" . ibuffer-backwards-next-marked)
("}" . ibuffer-forwards-next-marked)
("[" . ibuffer-backward-filter-group)
("]" . ibuffer-forward-filter-group)
("$" . ibuffer-toggle-filter-group))
:after (ibuffer))
(use-package consult-dir
:after vertico consult
:bind (([list-directory] . consult-dir)
:map vertico-map
("C-x C-d" . consult-dir)
("C-x C-j" . consult-dir-jump-file)))
Basic configuration nicked from Prot: https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/
(use-feature dired
:after vertico pdcmacs-global-bindings
:bind (("M-m a d" . dired)
("M-m j d" . dired-jump)
("M-m j D" . dired-jump-other-window)
:map dired-mode-map
(", w" . wdired-change-to-wdired-mode))
:config
(put 'dired-find-alternate-file 'disabled nil)
<<dired-config>>
:hook
(after-init . file-name-shadow-mode)
(rfn-eshadow-update-overlay . vertico-directory-tidy)
(dired-mode . dired-hide-details-mode)
:custom
(dired-dwim-target t)
(dired-guest-shell-alist-user
'(("\\.\\(png\\|jpe?g\\|tiff?\\)" "feh" "xdg-open")
("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open")
(".*" "xdg-open")))
(dired-recursive-copies 'always)
(dired-recursive-deletes 'always)
(dired-use-ls-dired nil)
(dired-omit-file-p t)
(dired-omit-files "^\\.?#"))
(use-package dired-subtree
:after dired
:bind
( :map dired-mode-map
("<tab>" . dired-subtree-toggle)
("TAB" . dired-subtree-toggle)
("<backtab>" . dired-subtree-remove)
("S-TAB" . dired-subtree-remove))
:custom
(dired-subtree-use-backgrounds nil))
(use-feature dired-x
:commands (dired-jump dired-jump-other-window dired-omit-mode))
(use-package casual-dired
:bind (:map dired-mode-map ("C-o" . casual-dired-tmenu)))
Taken from James Dyer
(defun pdc/dired-create-directory ()
"Wrapper to dired-create-directory to avoid minibuffer completion"
(interactive)
(let ((search-term
(read-from-minibuffer "Dir : ")))
(dired-create-directory search-term)))
(defun pdc/dired-create-empty-file ()
"Wrapper to `dired-create-empty-file' to avoid minibuffer expansion"
(interactive)
(let ((search-term
(read-from-minibuffer "File : ")))
(dired-create-empty-file search-term)))
(bind-keys :map dired-mode-map
("_" . pdc/dired-create-empty-file)
("+" . pdc/dired-create-directory))
Using the built in view-mode
works like a char, it converts buffers to view only and doesn’t allow them to be modified. The following added behaviour is nicked from http://yummymelon.com/devnull/enhancing-navigation-in-emacs-view-mode.html.
(use-feature view
:hook (view-mode . pdc/view-mode-hook)
:custom
(view-read-only t)
:preface
(defun pdc/view-mode-hook ()
(cond ((derived-mode-p 'org-mode)
(bind-keys :map view-mode-map
("p" . org-previous-visible-heading)
("n" . org-next-visible-heading)))
((derived-mode-p 'markdown-mode)
(bind-keys :map view-mode-map
("p" . markdown-outline-previous)
("n" . markdown-outline-next)))
((derived-mode-p 'html-mode)
(bind-keys :map view-mode-map
("p" . sgml-skip-tag-backward)
("n" . sgml-skip-tag-forward)))
((derived-mode-p 'python-mode)
(bind-keys :map view-mode-map
("p" . python-nav-backward-block)
("n" . python-nav-forward-block)))
((derived-mode-p 'emacs-lisp-mode)
(bind-keys :map view-mode-map
("p" . backward-sexp)
("n" . forward-sexp)))
((derived-mode-p 'makefile-mode)
(bind-keys :map view-mode-map
("p" . makefile-previous-dependency)
("n" . makefile-next-dependency)))
((derived-mode-p 'c-mode)
(bind-keys :map view-mode-map
("p" . c-beginning-of-defun)
("n" . c-end-of-defun)))
((derived-mode-p 'prog-mode)
(bind-keys :map view-mode-map
("p" . backward-sexp)
("n" . forward-sexp)))
(t
(bind-keys :map view-mode-map
("p" . scroll-down-command)
("n" . scroll-up-command))))))
There are different beginnings and endings within a line. I find it convenient to bounce to the beginning of the current string or comment, the beginning of the ‘logical’ line (ie. just after the indent) and sometimes even to column zero. The mwim
package does most of that, and it’s not hard to add functions to support jumping to the beginning and end of strings too.
(use-package mwim
:custom
(mwim-position-functions '(mwim-code-beginning
mwim-line-beginning
mwim-comment-beginning
+mwim-current-string-beginning
+mwim-current-string-end
mwim-code-end
mwim-line-end))
:bind (("C-a" . mwim-beginning)
("C-e" . +mwim-next-ending))
:config
(defun +mwim-next-ending ()
"Move point to the the nearest ending place"
(interactive "^")
(mwim-move-to-next-position mwim-end-position-functions #'<))
(defun +mwim-current-string-beginning ()
"Return position of the beginning of the current string.
Return nil if not inside a string (or already at the beginning of one)."
(let* ((syn (syntax-ppss))
(beg (and (nth 3 syn)
(nth 8 syn))))
(if beg (1+ beg))))
(defun +mwim-beginning-of-current-string ()
"Move point of the beginning of the current string.
If we're not in the body of a string, do nothing."
(interactive "^")
(when-let* ((string-beg (+mwim-current-string-beginning)))
(goto-char string-beg)))
(defun +mwim-current-string-end ()
"Return position of the end of the current string.
Return nil if not inside a string (or already at the end of one)."
(mwim-point-at (+mwim-end-of-current-string)))
(defun +mwim-end-of-current-string ()
"Move point to the end of the current string.
Do nothing if we're not in the body of a string."
(interactive "^")
(when-let* ((string-beg (+mwim-current-string-beginning)))
(goto-char (1- string-beg))
(forward-sexp)
(backward-char)))
(push '+mwim-current-string-beginning mwim-beginning-position-functions)
(push '+mwim-current-string-end mwim-end-position-functions))
Not sure I’ve put this in the right place, but it’s a start. imenu
allows for jumping about a buffer based on a mode specific index. I should remember it’s there more often.
(use-package imenu
:bind
(("M-m j i" . imenu))
:hook
(font-lock-mode . pdc/try-to-add-imenu)
:custom
(imenu-sort-function 'imenu--sort-by-name)
:init
(defun pdc/try-to-add-imenu ()
"Add Imenu to modes that have font-lock-mode activated."
(condition-case nil (imenu-add-to-menubar "Imenu")
(error nil))))
(use-package imenu-list
:custom
(imenu-list-focus-after-activation t)
(imenu-list-auto-resize t)
(imenu-list-position 'left)
(imenu-list-size 40))
(use-package avy
:bind (("M-m j '" . avy-goto-char)
("M-m j ;" . avy-goto-char-timer))
:custom
(avy-timeout-seconds 0.3)
(avy-single-candidate-jump nil)
:config
(defun pdc/avy-action-embark (pt)
(unwind-protect
(save-excursion
(goto-char pt)
(embark-act))
(select-window
(cdr (ring-ref avy-ring 0))))
t)
(setf (alist-get ?. avy-dispatch-alist) 'pdc/avy-action-embark))
(use-package casual-avy
:bind ("M-m j A" . casual-avy-tmenu)
:after avy)
(use-package ace-window
:bind
([other-window] . ace-window)
("M-m w o" . ace-window)
("M-m w s" . ace-swap-window))
(use-package dogears
:hook emacs-startup
:bind (("M-m g d" . dogears-go)
("M-m g M-b" . dogears-back)
("M-m g M-f" . dogears-forward)
("M-m g M-d" . dogears-list)
("M-m g M-D" . dogears-sidebar)))
We want to keep buffers in sync with their underlying files (and directories) so we use autorevert
(use-feature autorevert
:custom
(global-auto-revert-non-file-buffers t)
(auto-revert-verbose nil)
:hook (after-init . global-auto-revert-mode))
I’m not a fan of trailing white space, nor am I fan of surprise whitespace diffs on lines I didn’t touch when adding changes to git. ws-butler-mode
tidies up trailing whitespace on file save, but only on lines I modified. Perfect!
(use-package ws-butler
:diminish
:hook
((prog-mode text-mode) . ws-butler-mode))
Trying out jinx
from https://gihub.com/minad/jinx, enabling globally.
(use-package jinx
:hook (emacs-startup . global-jinx-mode)
:bind (("M-$" . jinx-correct)
("C-M-$" . jinx-languages))
:custom (jinx-languages "en_GB")
:diminish " 🅙"
:config
(defun +jinx--add-to-abbrev (overlay word)
"Add abbreviation to `global-abbrev-table`.
The misspelled word is taken from OVERLAY. WORD is the corrected word."
(let ((abbrev (buffer-substring-no-properties
(overlay-start overlay)
(overlay-end overlay))))
(message "Abbrev: %s -> %s" abbrev word)
(define-abbrev global-abbrev-table abbrev word)))
(advice-add 'jinx--correct-replace :before #'+jinx--add-to-abbrev))
Pulse the current line or region on demand, and after certain commands.
(defun pulse-line (&rest _)
"Pulse the current line."
(pulse-momentary-highlight-one-line (point)))
(defun pulse-line-command ()
"Interactively pulse the current line."
(interactive)
(pulse-line))
(defun pdc/pulse-current-region (&rest _)
"Pulse the current implicit or active region."
(if mark-active
(pulse-momentary-highlight-region (region-beginning) (region-end))
(pulse-momentary-highlight-region (point) (mark))))
(defun pdc/pulse-current-region ()
"Interactively pulse the current implicit or active region."
(interactive)
(pdc/pulse-current-region))
(defun pdc-reveal-entry ()
"Reveal Org or Outline entry and pulse the current line."
(cond
((and (eq major-mode 'org-mode)
(org-at-heading-p))
(org-show-entry))
((and (or (eq major-mode 'outline-mode)
(bound-and-true-p outline-minor-mode))
(outline-on-heading-p))
(outline-show-entry))))
(defun pdc-recenter ()
(recenter-top-bottom 2))
(dolist (command '(scroll-up-command
scroll-down-command
recenter-top-bottom
other-window))
(advice-add command :after #'pulse-line))
(advice-add #'kill-ring-save :before #'pdc/pulse-current-region)
(bind-keys
("C-c h p" . pulse-line-command))
(add-hook 'minibuffer-setup-hook #'pulse-line)
(add-hook 'consult-after-jump-hook #'pdc-recenter)
(add-hook 'consult-after-jump-hook #'pdc-reveal-entry)
(add-hook 'imenu-after-jump-hook #'pdc-recenter)
(add-hook 'imenu-after-jump-hook #'pdc-reveal-entry)
(add-hook 'occur-mode-find-occurrence-hook #'pdc-recenter)
(add-hook 'occur-mode-find-occurrence-hook #'pulse-line)
Some modes are less confusing if the current line is always highlighted though.
(use-feature hl-line-mode
:hook
((occur-mode dired-mode package-menu-mode) . hl-line-mode))
Like paredit
but for more modes…
(use-package smartparens
:diminish
:hook
(((org-mode css-mode python-mode) . smartparens-mode)
(minibuffer-setup . turn-on-smartparens-strict-mode)
(emacs-startup . show-smartparens-global-mode))
:config
(require 'smartparens-config)
(sp-with-modes '(minibuffer-inactive-mode minibuffer-mode)
(sp-local-pair "'" nil :actions nil)
(sp-local-pair "(" nil :wrap "C-("))
(sp-with-modes 'org-mode
(sp-local-pair "=" "=" :wrap "C-=")
(sp-local-pair "/" "/")
(sp-local-pair "~" "~"))
(sp-with-modes 'web-mode
(sp-local-pair "{{#if" "{//if}")
(sp-local-pair "{{#unless" "{//unless"))
(sp-with-modes '(tex-mode plain-tex-mode latex-mode)
(sp-local-tag "i" "\"<" "\">"))
(sp-use-paredit-bindings))
“Yeah, yeah,” the purists will tell you, “Emacs isn’t really set up to handle multiple cursors efficiently, you’re better using keyboard macros!” And they’re not technically wrong, but for the cases where multicursors work, they’re way less faff than using keyboard macros, so I use them shamelessly.
I’m experimenting with mc/mark-more-like-this-extended
and other mark-more
stuff
(use-package multiple-cursors
:after transient
:bind
(:prefix "M-m m"
:prefix-map pdc-multi-map
:prefix-docstring "multi"
("a" . mc/edit-beginnings-of-lines)
("e" . mc/edit-ends-of-lines)
("^" . mc/edit-beginnings-of-lines)
("$" . mc/edit-ends-of-lines)
("m" . mc/edit-lines)
("C-o" . mc/mark-more-tmenu))
(:map
mc/keymap
("RET" . multiple-cursors-mode))
:init
(transient-define-suffix tsc-suffix-print-args (the-prefix-arg)
"Report the PREFIX-ARG, prefix's scope, and infix values."
;; :transient 'transient--do-call
(interactive "P")
(let* ((args (transient-args (oref transient-current-prefix command)))
(scope (oref transient-current-prefix scope))
(marking-mode (or (transient-arg-value "--mode=" args)
(if (region-active-p) "region" "word"))))
(message "prefix-arg: %s \nprefix's scope value: %s \ntransient-args: %s\nmarking-mode: %s"
the-prefix-arg scope args marking-mode)))
(transient-define-argument mc/mode-select ()
"Select the multi marking mode"
:class 'transient-switches
:argument-format "--by-%s"
:argument-regexp "\\(--by-\\(word\\|symbol\\|line\\|region\\)"
:choices '("word" "symbol" "line" "region"))
(defun +mc--default-marking-mode ()
(if (region-active-p) "region" "line"))
(defvar +mc--marking-functions
'((forward . ((word . mc/mark-next-like-this-word)
(symbol . mc/mark-next-like-this-symbol)
(line . mc/mark-next-lines)
(region . mc/mark-next-like-this)))
(backward . ((word . mc/mark-previous-like-this-word)
(symbol . mc/mark-previous-like-this-symbol)
(line . mc/mark-previous-lines)
(region . mc/mark-previous-like-this)))))
(defun +mc/mark-according-to-mode (direction)
(let* ((args (transient-args (oref transient-current-prefix command)))
(mode (intern (or (transient-arg-value "--mode=" args)
(+mc--default-marking-mode))))
(fn (alist-get mode (alist-get direction +mc--marking-functions))))
(funcall fn 1)))
(defun +mc/mark-more-quit-label ()
(if +mc/marking-direction "quit marking" "quit"))
(transient-define-suffix +mc/mark-more-quit ()
:transient nil
:key "C-g"
:description "quit"
(interactive)
(message "Quitting")
(deactivate-mark)
(mc/disable-multiple-cursors-mode)
(setq +mc/marking-direction nil)
(transient-quit-all))
(transient-define-suffix +mc/mark-more-finish ()
:transient nil
:key "RET"
:description "finish marking"
(interactive)
(setq +mc/marking-direction nil))
(defun +mc/mark-more-finish-fn ()
(interactive)
(setq +mc/marking-direction nil))
(transient-define-prefix mc/mark-more-tmenu ()
:incompatible '(("--mode=word" "--mode=symbol" "--mode=line" "--mode=region"))
["Incremental"
["Mode"
("w" "word" "--mode=word" :transient t)
("s" "symbol" "--mode=symbol" :transient t)
("l" "line" "--mode=line" :transient t)
("r" "region" "--mode=region" :transient t :if region-active-p)]
["Mark"
("n" "next" +mc/transient-marking--down :transient t)
("p" "prev" +mc/transient-marking--up :transient t)
(">" "next" +mc/transient-marking--down :transient t)
("<" "prev" +mc/transient-marking--up :transient t)
("{" (lambda () (if (eq +mc/marking-direction 'up) "skip" "remove"))
+mc/transient-marking--left :transient t)
("}" (lambda () (if (eq +mc/marking-direction 'up) "remove" "skip"))
+mc/transient-marking--right :transient t)]]
["Done"
("S" "show arguments" tsc-suffix-print-args :transient t)]
[:class transient-row
(+mc/mark-more-quit)
;; ("RET" "finish" +mc/mark-more-finish-fn)
(+mc/mark-more-finish)
;; ("RET" "finish" (lambda ()
;; (interactive)
;; (setq +mc/marking-direction nil)))
]
(interactive)
(setq +mc/marking-direction nil)
(transient-setup
'mc/mark-more-tmenu nil nil
:value (list (format "--mode=%s" (+mc--default-marking-mode)))))
(defvar +mc/marking-direction nil)
;; (defvar +mc/marking-mode 'region)
(defun +mc/transient-marking--up (&rest _)
(interactive)
(+mc/mark-according-to-mode 'backward)
(setq +mc/marking-direction 'up))
(defun +mc/transient-marking--down (&rest _)
(interactive)
(+mc/mark-according-to-mode 'forward)
(setq +mc/marking-direction 'down))
(defun +mc/transient-marking--left (&rest _)
(interactive)
(if (eq +mc/marking-direction 'down)
(mc/unmark-next-like-this)
(mc/skip-to-previous-like-this)))
(defun +mc/transient-marking--right (&rest _)
(interactive)
(if (eq +mc/marking-direction 'up)
(mc/unmark-previous-like-this)
(mc/skip-to-next-like-this)))
:config
(dolist (cmd '(+mc/transient-marking--right
+mc/transient-marking--left
+mc/transient-marking--down
+mc/transient-marking--up))
(add-to-list 'mc/cmds-to-run-once cmd)))
;; (use-package phi-search)
;; (use-package phi-search-mc :config (phi-search-mc/setup-keys))
(use-package mc-extras
:bind
(:map
mc/keymap
("M-m m =" . mc/compare-chars)
("M-m m ." . mc/move-to-column)))
The unfill
package lets me toggle between filled and unfilled variants of a line/para.
(use-package unfill
:bind ([remap fill-paragraph] . unfill-toggle))
An emacs builtin, we’re just configuring it.
(use-feature recentf
:hook
after-init
(find-file . pdc/recentf-find-file-hook)
:custom
(recentf-max-saved-items 1000)
(recentf-auto-cleanup 'never)
(recentf-auto-save-timer (run-with-idle-timer 600 t 'recentf-save-list))
(recentf-max-menu-items 25)
(recentf-save-file-modes nil)
(recentf-auto-cleanup nil)
:init
(defun pdc/recentf-find-file-hook ()
(unless recentf-mode
(recentf-mode)
(recentf-track-opened-file)))
:config
(add-to-list 'recentf-exclude no-littering-etc-directory)
(add-to-list 'recentf-exclude (expand-file-name package-user-dir))
(add-to-list 'recentf-exclude "COMMIT_EDITMSG\\'"))
Let’s try vundo
for a bit
(use-package vundo
:bind
("M-m a u" . vundo)
:custom
(vundo-compact-display t)
(vundo-window-max-height 8)
(vundo-glyph-alist vundo-unicode-symbols))
(use-package markdown-mode
:mode (("README\\.md\\'" . gfm-mode)
("\\.\\(?:md\\|markdown\\|mkdn?\\|mdown\\|mdwn\\)\\'" . markdown-mode))
:custom
(markdown-command "multimarkdown | pandoc"))
Let emacs guess and set the separator for csv
files.
(use-package csv-mode
:hook (csv-mode . csv-guess-set-separator)
:mode ("\\.csv\\'" . csv-mode))
YAML Ain’t Markup Language, but it is almost as ubiquitous as CSV, so let’s load it up here.
(use-feature yaml-mode
:mode "\\.ya?ml\\'")
(use-package yasnippet
:demand t
:mode ("~/.config.*/snippets/" . snippet-mode)
:commands yas-hippie-try-expand
:bind (:map
yas-minor-mode-map
("\t" . hippie-expand))
:hook
(emacs-startup . yas-global-mode)
:diminish yas-minor-mode
:init
(with-eval-after-load 'hippie-expand
(add-hook 'hippie-expand-try-functions-list 'yas-hippie-try-expand))
:custom
(yas-key-syntaxes '("w_" "w_." "^ "))
(yas-expand-only-for-last-commands nil)
(yas-triggers-in-field t)
(yas-wrap-around-region t)
(yas-prompt-functions '(yas-completing-prompt))
:init
(defvar pdc-snippet-dirs (seq-filter 'file-directory-p
(list (expand-file-name "snippets/" user-emacs-directory)
(expand-file-name "~/.config/snippets"))))
(setq yas-snippet-dirs pdc-snippet-dirs))
(use-package yasnippet-snippets :after yasnippet)
(use-package consult-yasnippet :after (consult yasnippet)
:bind
(("M-g y" . consult-yasnippet)
:map yas-minor-mode-map))
Make life easier when writing plain-ish text in Emacs.
Not sure how I feel about emacs suggesting improvements on my grammar, but let’s give writegood-mode
a go.
The default writegood-mode
highlighting faces are horrid, so we need to fix them at some point.
(use-package writegood-mode
:diminish
:bind ("M-m W" . writegood-mode)
:custom
(writegood-weasel-words
'("one of the"
"should"
"just"
"sort of"
"a lot"
"probably"
"maybe"
"perhaps"
"I think"
"really"
"pretty"
"nice"
"action"
"utilize"
"leverage"))
:hook
(text-mode . writegood-mode)
((view-mode emacs-news-view-mode) . (lambda () (writegood-mode nil)))
:config
(writegood-weasels-turn-on)
(writegood-passive-voice-turn-on)
(writegood-duplicates-turn-on))
Let’s try writeroom
mode. If it turns out to be crap, there’s also darkroom
and olivetti
that purport to do similar things.
(use-package writeroom-mode
:bind (:map writeroom-mode-map
("<C-M-left>" . writeroom-decrease-width)
("<C-M-right>" . writeroom-increase-width)
("C-M-=" . writeroom-adjust-width)
("M-m , [" . writeroom-decrease-width)
("M-m , ]" . writeroom-increase-width)
("M-m , =" . writeroom-adjust-width))
:custom
(writeroom-mode-line '(" " global-mode-string))
(writeroom-local-effects '(display-time-mode))
:config
(advice-add 'text-scale-adjust :after 'visual-fill-column-adjust))
emacs-powerthesaurus
is a plugin to integrate Emacs with the powerthesaurus.org service. Not sure I’ll actually use this, but why not try it?
(use-package powerthesaurus)
Sometimes it’s useful to have multiple interfaces to a thing because we have multiple states of mind. Sorry… I have multiple states of mind. We already have consult-ripgrep
in place, but let’s try deadgrep too
(use-package deadgrep
:bind (("M-s R" . deadgrep)))
calc
and casual-calc
are rather fine.
(use-feature calc)
(use-package casual-calc
:bind (:map
calc-mode-map ("C-o" . 'casual-calc-tmenu)
:map
calc-alg-map ("C-o" . 'casual-calc-tmenu))
:after (calc))
Let’s try the drag-stuff
minor mode. M-up
, M-down
, M-right
and M-left
will drag the things.
(use-package drag-stuff
:diminish
:hook (emacs-startup . drag-stuff-global-mode)
:config
(drag-stuff-define-keys))
Lifted from Prot’s snippets.
(use-feature delsel
:hook (after-init . delete-selection-mode))
Yeah, sometimes I want to edit /etc/whatever
and I don’t want to have to leave Emacs to do so. So let’s try sudo-edit
(use-package sudo-edit)
Live everyone else and their sibling, I use orderless
, corfu
, consult
, embark
, marginalia
and vertico
as the current fleet of completion related packages that work, when I configure them right.
I like to save the history of the mini-buffer
(use-package savehist
:hook (emacs-startup . savehist-mode)
:custom
(savehist-file (no-littering-expand-var-file-name "savehist"))
(history-length 100)
(history-delete-duplicates t)
(savehist-save-minibuffer-history t)
(savehist-save-minibuffer-history t "Save minibuffer history")
(savehist-additional-variables '(kill-ring
search-ring
regexp-search-ring
register-alist)
"Save more histories"))
And remembering the state of the *scratch*
buffer is handy too.
(use-package persistent-scratch
:init (persistent-scratch-setup-default))
We use the in-built dabbrev
package. It doesn’t need much configuration, but it doesn’t hurt to do some.
(use-feature dabbrev
:commands (dabbrev-expand dabbrev-completion)
:custom
(dabbrev-abbrev-char-regexp "\\sw\\|\\s_")
(dabbrev-abbrev-skip-leading-regexp "[$*/=~']")
(dabbrev-backward-only nil)
(dabbrev-case-distinction 'case-replace)
(dabbrev-check-other-buffers t)
(dabbrev-eliminate-newlines t)
(dabbrev-upcase-means-case-search t)
(dabbrev-ignored-buffer-modes
'(archive-mode image-mode doc-view-mode pdf-view-mode tags-table-mode)))
(use-package emacs
:bind ( ("M-/" . 'hippie-expand))
:custom
(hippie-expand-try-functions-list
'(yas-hippie-try-expand
try-expand-all-abbrevs
try-complete-file-name-partially
try-complete-file-name
try-expand-dabbrev
try-expand-dabbrev-from-kill
try-expand-dabbrev-all-buffers
try-expand-list
try-expand-line
try-complete-lisp-symbol-partially
try-complete-lisp-symbol))
:config
(remove-hook 'save-some-buffers-functions 'abbrev--possibly-save))
(use-feature abbrev
:diminish
:hook emacs-startup)
Let’s set up the minibuffer
to play nicely with the completion frameworks we’re going to use.
(use-feature minibuffer
:custom
(completions-format 'one-column)
(completion-auto-help 'always)
(completion-auto-select t)
(completions-detailed t)
(completion-show-inline-help t)
(completions-max-height 48)
(completions-highlight-face 'completions-highlight)
(minibuffer-completion-auto-choose t)
(completion-styles '(basic substring initials flex orderless))
(completion-category-defaults nil)
(completion-category-overrides
'((file (styles . (basic partial-completion orderless)))
(command (styles . (basic partial-completion orderless)))
(bookmark (styles . (basic substring)))
(library (styles . (basic substring)))
(embark-keybinding (styles . (basic substring)))
(imenu (styles . (basic substring orderless)))
(consult-location (styles . (basic substring orderless)))
(kill-ring (styles . (emacs22 orderless)))
(eglot (styles . (emacs22 substring orderless))))))
Sometimes, it’s nice to edit the contents of the mini-buffer in a full buffer. So I’ll add the miniedit
package. This binds C-M-e
within a minibuffer to throw the content into a temporary buffer for editing
(use-package miniedit
:commands minibuffer-edit
:init (miniedit-install))
The perfect in-buffer pop-up completion system doesn’t exist. Or, at least, I’ve yet to find it. corfu
in conjunction with vertico
etc is about as good as I’ve found.
(use-package corfu
:after savehist
:custom
;; Works with `indent-for-tab-command'. Make sure tab doesn't indent when you
;; want to perform completion
(tab-always-indent 'complete)
(tab-first-completion 'word)
(completion-cycle-threshold 3)
(corfu-cycle t)
(corfu-auto t)
(corfu-auto-prefix 3)
(corfu-auto-delay 0.2)
(corfu-preview-current nil)
(corfu-quit-at-boundary 'separator)
(global-corfu-modes '((not org-mode) prog-mode))
(corfu-preselect nil)
;; quarantine
(corfu-history-mode 1)
(corfu-popupinfo-delay '(1.25 . 0.5))
:config
(add-to-list 'savehist-additional-variables 'corfu-history)
:hook
(eshell-history-mode . +eshell-history-mode-setup-completion)
(lsp-completion-mode . +lsp-mode-setup-completion)
;; (after-init . global-corfu-mode)
;; (after-init . corfu-popupinfo-mode)
:bind
(:map corfu-map
("M-SPC" . corfu-insert-separator)
("RET" . corfu-insert)
("M-RET" . newline-and-indent)
("S-<return>" . corfu-insert)
("M-m" . +corfu-move-to-minibuffer)
("TAB" . +pdc/corfu-complete-common-or-next)
("<tab>" . +pdc/corfu-complete-common-or-next))
:init
(global-corfu-mode)
(corfu-popupinfo-mode)
;; TODO: Write a function to attach to tab that first completes a common prefix and, on second hit, inserts the current selection
(defun +pdc/corfu-complete-common-or-next ()
"Complete common prefix or go to next candidate."
(interactive)
(if (= corfu--total 1)
(progn
(corfu--goto 1)
(corfu-insert))
(let* ((input (car corfu--input))
(str (if (thing-at-point 'filename) (file-name-nondirectory input) input))
(pt (length str))
(common (try-completion str corfu--candidates)))
(if (and (> pt 0)
(stringp common)
(not (string= str common)))
(insert (substring common pt))
(corfu-next)))))
(defun +pdc/corfu-insert ()
"Insert current candidate or newline."
(interactive))
(defun +corfu-move-to-minibuffer ()
(interactive)
(let (completion-cycle-threshold completion-cycling)
(apply #'consult-completion-in-region completion-in-region--data)))
(defun +lsp-mode-setup-completion ()
(setf (alist-get 'styles (alist-get 'lsp-capf completion-category-defaults))
'(orderless)))
(defun +eshell-history-mode-setup-completion ()
(setq-local corfu-quit-at-boundary t
corfu-quit-no-match t
corfu-auto nil)
(corfu-mode t)))
(use-package corfu-terminal
:if
(not window-system)
:init
(corfu-terminal-mode t))
As well as corfu, cape
does some good stuff with completion-at-point
.
(use-package cape
:preface
(bind-keys :prefix "M-m ."
:prefix-map pdc-completion-at-point-map
:prefix-docstring "completion…")
:bind (:map
pdc-completion-at-point-map
("p" . completion-at-point)
("t" . complete-tag)
("d" . cape-dabbrev)
("h" . cape-history)
("f" . cape-file)
("k" . cape-keyword)
("s" . cape-symbol)
("a" . cape-abbrev)
("l" . cape-line)
("w" . cape-dict)
("\\" . cape-tex)
("_" . cape-tex)
("^" . cape-tex)
("&" . cape-sgml)
("r" . cape-rfc1345))
:init
(add-to-list 'completion-at-point-functions #'cape-dabbrev)
(add-to-list 'completion-at-point-functions #'cape-abbrev)
(add-to-list 'completion-at-point-functions #'cape-file)
(add-to-list 'completion-at-point-functions #'cape-elisp-block)
(add-to-list 'completion-at-point-functions #'cape-history)
(add-to-list 'completion-at-point-functions #'cape-keyword)
(add-to-list 'completion-at-point-functions #'cape-tex))
The consult
package provides a way to search, filter, preview and select entries based on lists provided by completion-at-point. I’ve also added
consult-yasnippet
to help expandyasnippet
(use-package consult
:hook (completion-list-mode . consult-preview-at-point-mode)
:custom
(register-preview-delay 0.5)
(register-preview-function #'consult-register-format)
(consult-line-numbers widen t)
(consult-async-min-input 3)
(consult-async-input-debounce 0.5)
(consult-async-input-throttle 0.8)
(consult-narrow-key "<")
(consult-preview-key 'any)
:init
(advice-add #'register-preview :override #'consult-register-window)
(with-eval-after-load 'xref
(setq xref-show-xrefs-function #'consult-xref
xref-show-definitions-function #'consult-xref))
:config
(bind-keys ([remap isearch-forward] . consult-line)
([remap Info-search] . consult-info)
([remap imenu] . consult-imenu)
([remap recentf-open-files] . consult-recent-file)
("C-x M-:" . consult-complex-command)
("C-x b" . consult-buffer)
("C-x 4 b" . consult-buffer-other-window)
("C-x 5 b" . consult-buffer-other-frame)
("C-x r b" . consult-bookmark)
("C-x p b" . consult-project-buffer)
("M-#" . consult-register-load)
("M-'" . consult-register-store)
("C-M-#" . consult-register)
("M-y" . consult-yank-pop)
:map isearch-mode-map
("M-e" . consult-isearch-history)
("M-s e" . consult-isearch-history)
("M-s l" . consult-line)
("M-s L" . consult-line-multi)
:map minibuffer-local-map
("C-s" ("insert-current-symbol" . (lambda ()
"Insert the current symbol"
(interactive)
(insert (save-excursion
(set-buffer (window-buffer (minibuffer-selected-window)))
(or (thing-at-point 'symbol t) ""))))))
("M-s" . consult-history)
("M-r" . consult-history)
:map search-map
("d" . consult-find)
("D" . consult-locate)
("g" . consult-grep)
("G" . consult-git-grep)
("r" . consult-ripgrep)
("l" . consult-line)
("L" . consult-line-multi)
("k" . consult-keep-lines)
("u" . consult-focus-lines)
("e" . consult-isearch-history))
(consult-customize
consult-goto-line
consult-theme :preview-key '(:debounce 0.4 any))
:demand t)
The embark
package is analogous to the right click menu, but rather more… more.
(use-package embark
:preface
;; Make sure embark-act wins the binding race with xref
(require 'xref nil t)
:bind
(("C-." . embark-act)
("M-." . embark-act)
("M-," . embark-dwim)
("C-;" . embark-dwim)
("M-m ." . embark-act)
("M-m ;" . embark-dwim)
(([remap describe-bindings] . embark-bindings))
("C-h B" . embark-bindings)
:map embark-file-map
("V" . view-file))
:custom
(embark-cycle-key "M-.")
(prefix-help-command #'embark-prefix-help-command)
(embark-confirm-act-all nil)
(embark-mixed-indicator-both nil)
(embark-mixed-indicator-delay nil)
(embark-indicators '(embark-mixed-indicator embark-highlight-indicator))
(embark-verbose-indicator-nested nil)
(embark-verbose-indicator-buffer-sections '(bindings))
(embark-verbose-indicator-excluded-actions '(embark-cycle embark-act-all embark-collect embark-export embark-insert)))
(use-package embark-consult
:after consult
:hook
(embark-collect-mode . consult-preview-at-point-mode))
Using vertico
and orderless
together makes for a rather pleasant experience.
(use-package vertico
:after consult
:custom
(vertico-cycle t)
(vertico-scroll-margin 0)
(vertico-count 5)
(vertico-resize t)
(vertico-multiform-mode 1)
(vertico-multiform-commands
'((consult-recent-file buffer)
(consult-mode-command buffer)
(consult-complex-command buffer)
(embark-bindings buffer)
(consult-locate buffer)
(consult-project-buffer buffer)
(consult-ripgrep buffer)
(consult-fd buffer)))
(vertico-multiform-categories '((buffer flat (vertico-cycle . t))))
:hook
(after-init . vertico-mode)
:bind
(:map vertico-map
:prefix "M-,"
:prefix-map vertico-options-map
("r" . vertico-reverse-mode)
("g" . vertico-grid-mode))
(:map vertico-map
("M-q" . vertico-quick-insert)
("C-q" . vertico-quick-exit)
("C-k" . kill-whole-line)
("C-u" . kill-whole-line)
("C-o" . vertico-next-group)
("<tab>" . vertico-insert)
("TAB" . vertico-insert)
("M-<return>" . minibuffer-force-complete)))
(use-package emacs
:init
(setq minibuffer-prompt-properties
'(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
(setq enable-recursive-minibuffers t))
(use-feature vertico-directory
:after vertico
:bind
(:map vertico-map
("RET" . vertico-directory-enter)
("DEL" . vertico-directory-delete-char)
("M-DEL" . vertico-directory-delete-word))
:hook
(rfn-eshadow-update-overlay . vertico-directory-tid))
Completing the group, we have orderless
a pattern matching package for parsing user input and turning it into patterns that match against completing-read
. I usually just require it and leave it alone, but I’m trying out some fancy stuff from https://github.com/minad/wiki#minads-orderless-configuration because, why not?
(use-package orderless
:commands (orderless-define-completion-style)
:after minibuffer
:init
(defun +orderless--consult-suffix ()
"Regexp which matches the end of string with Consult tofu support."
(if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
(format "[%c-%c]*$"
consult--tofu-char
(+ consult--tofu-char consult--tofu-range -1))
"$"))
(defun +orderless-consult-dispatch (word _index _total)
(cond
((string-suffix-p "$" word)
`(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--consult-suffix))))
((and (or minibuffer-completing-file-name
(derived-mode-p 'eshell-mode))
(string-match-p "\\`\\.." word))
`(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--consult-suffix))))))
(orderless-define-completion-style +orderless-with-initialism
(orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil)
(orderless-component-separator #'orderless-escapable-split-on-space)
(orderless-style-dispatchers (list #'+orderless-consult-dispatch
#'orderless-affix-dispatch))
:config
(dolist
(kv '((file (styles +orderless-with-initialism))
(command (styles +orderless-with-initialism))
(variable (styles +orderless-with-initialism))
(symbol (styles +orderless-with-initialism))
(consult-location (styles +orderless-with-initialism))))
(setf (alist-get (car kv) completion-category-overrides)
(cdr kv)))
:bind (:map minibuffer-local-completion-map
("SPC" . nil)
("?" . nil)))
This annotates completion targets rather nicely. Mostly, it just works.
(use-package marginalia
:hook (emacs-startup . marginalia-mode)
:bind (("M-A" . marginalia-cycle)
:map minibuffer-local-map
("M-A" . marginalia-cycle))
:custom
(marginalia-annotators '(marginalia-annotators-light nil)))
(use-package nerd-icons-completion
:after marginalia
:hook
(emacs-startup . nerd-icons-completion-mode)
(marginalia-mode . nerd-icons-completion-marginalia-setup))
A few mods:
- Paths for shell execution are fetched and stored
Eshell
is customised to start from the top of the page- Shell commands can be executed using https://github.com/xenodium/dwim-shell-command
(use-package exec-path-from-shell
:config
(when (memq window-system '(mac ns x))
(exec-path-from-shell-initialize)))
(use-feature eshell
:custom
(eshell-where-to-jump 'begin)
(eshell-review-quick-commands nil)
(eshell-smart-space-goes-to-end t))
(use-package dwim-shell-command
:after dired
:bind (([remap shell-command] . dwim-shell-command)
:map dired-mode-map
([remap dired-do-async-shell-command] . dwim-shell-command)
([remap dired-do-shell-command] . dwim-shell-command)
([remap dired-smart-shell-command] . dwim-shell-command))
:custom (dired-dwim-target t))
In writing modes, I like the way visual-fill-column-mode
handles things, centring the text block in the window.
(use-package visual-fill-column
:defer nil
:hook
((text-mode org-mode) . visual-fill-column-mode)
:custom
(visual-fill-column-enable-sensible-window-split t)
(visual-fill-column-center-text t))
It’s almost reached the point where it’s not really an Emacs configuration if it doesn’t include org-mode
, especially if it’s a literate configuration, so let’s get it loaded up. We use use-feature
here rather than use-package
to ensure we get the version packaged with emacs. This may be a mistake :)
Let’s get org-mode
and org-contrib
loaded first, then we can style it out with supporting packages.
(use-package org
:mode ("\\.txt$" . org-mode)
:diminish org-src-mode
:bind-keymap
:bind
(:map org-mode-map
("C-M-<return>" . org-insert-subheading)
("C-c M-<return>" . org-insert-subheading)
("C-c M-RET" . org-insert-subheading)
("C-M-i" . completion-at-point)
("C-M-w" . append-next-kill)
("M-m , v" . org-show-todo-tree)
("M-m , r" . org-refile)
("M-m , R" . org-reveal))
:preface
<<org-preface>>
:init
(bind-keys :prefix "M-m o"
:prefix-map pdc-org-prefix
:prefix-docstring "org"
("c" . org-capture)
("A" . org-agenda)
("l" . org-store-link)
("L" . org-insert-link-global)
("O" . org-open-at-point-global))
(defun my-adjoin-to-list-or-symbol (element list-or-symbol)
(let ((list (if (not (listp list-or-symbol))
(list list-or-symbol)
list-or-symbol)))
(require 'cl-lib)
(cl-adjoin element list)))
(defvar org-directory "~/Documents/org")
(defvar pdc/org-inbox-file
(+org-file-path "inbox.org"))
<<org-init>>
:custom
(org-auto-align-tags nil)
(org-tags-column 0)
(org-catch-invisible-edits 'show-and-error)
(org-special-ctrl-a/e t)
(org-insert-heading-respect-content t)
(org-hide-emphasis-markers t)
(org-pretty-entities t)
(org-ellipsis "…")
(org-agenda-block-separator ?—)
(org-agenda-time-grid
'((daily today require-timed)
(800 1000 1200 1400 1600 1800 2000)
" ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"))
(org-agenda-current-time-string
"←⭠ now ───────────────────────────────────────────────")
(org-agenda-start-with-log-mode t)
(org-log-done 'time)
(org-log-into-drawer t)
(org-pretty-entities t)
(org-use-sub-superscripts "{}")
(org-startup-with-inline-images (display-graphic-p))
(org-image-actual-width '(300))
(org-structure-template-alist
(append '(("c" . "center")
("C" . "comment")
("e" . "example")
("q" . "quote")
("E" . "export")
("h" . "export html")
("a" . "export ascii")
("M" . "export markdown")
("m" . "markdown")
("s" . "src")
("v" . "verse"))
'(
<<org-structure-templates>>)))
(org-indent-indentation-per-level 2)
(org-edit-src-content-indentation 0)
(org-src-preserve-indentation t)
(org-src-strip-leading-and-trailing-blank-lines t)
(org-src-tab-acts-natively t)
(org-footnote-auto-adjust t)
(org-adapt-indentation 'headline-data)
(org-return-follows-link t)
(org-special-ctrl-k t)
(org-use-speed-commands
(lambda ()
(and (looking-at org-outline-regexp-bol)
(not (org-in-src-block-p)))))
(org-loop-over-headlines-in-active-region t)
(org-blank-before-new-entry '((heading . t) (plain-list-item . auto)))
(org-cycle-include-plain-lists nil)
(org-support-shift-select t)
;; From Sacha Chua
(org-todo-keyword-faces
(when (fboundp 'modus-themes-get-color-value)
`(("TODO" . (:foreground ,(modus-themes-get-color-value 'blue-warmer) :weight bold))
("DONE" . (:foreground ,(modus-themes-get-color-value 'green-warmer) :weight bold))
("WAITING" . (:foreground ,(modus-themes-get-color-value 'red-warmer) :weight bold))
("SOMEDAY" . (:foreground ,(modus-themes-get-color-value 'fg-dim) :weight bold)))))
<<org-custom>>
:hook
(org-mode . visual-line-mode)
<<org-hook>>
:config
(for-gui
(add-hook 'org-mode-hook 'variable-pitch-mode))
;; (dolist (face '(org-code org-block))
;; (set-face-attribute
;; face nil
;; :inherit
;; (my-adjoin-to-list-or-symbol 'fixed-pitch
;; (face-attribute face :inherit))))
<<org-config>>)
(use-package org-mac-link)
(use-package org-notify)
(use-package org-contrib
:after (org)
:custom
(org-modules '(ol-bbdb
ol-bookmark
ol-elisp-symbol
ol-eshell
ol-info
ol-man
org-annotate-file
org-choose
org-collector
org-expiry
org-interactive-query
org-mac-iCal
org-mac-link
org-mouse
org-notify
org-panel
org-protocol
org-registry
org-screen
org-tempo
org-toc
orgtbl-sqlinsert))
:config
(org-load-modules-maybe))
In a gui environment, org-modern
looks great.
(use-package org-modern
:after org
:if (display-graphic-p)
:straight
(:host github :repo "minad/org-modern")
:hook
(org-mode . org-modern-mode)
(org-modern-mode . pdc/maybe-toggle-org-bullets-mode)
:init
(let (old-org-bullets-mode)
(defun pdc/maybe-toggle-org-bullets-mode ()
(if org-modern-mode
(when (featurep 'org-bullets)
(setq old-org-bullets-mode (if (boundp 'org-bullets-mode)
org-bullets-mode nil))
(org-bullets-mode nil))
(when (featurep 'org-bullets)
(org-bullets-mode old-org-bullets-mode)))))
:config
(setq org-modern-keyword
'((t . t)
("bibliography" . " ")
("cite_export" . " ")
("include" . "⇤")
("setupfile" . "⇚")
("html_head" . "🅷")
("html" . "🅗")
("latex_class" . "🄻")
("latex_header" . "🅻")
("latex" . "🅛")
("beamer_theme" . "🄱")
("beamer_header" . "🅱")
("beamer" . "🅑")
("attr_latex" . "🄛")
("attr_html" . "🄗")
("attr_org" . "⒪")
("header" . "›")
("caption" . "≌")
("name" . "⋮")
("results" . "∴")))
(setq org-modern-block-name
'((t . t)
("src" "»" "∥")
("example" "»-" "∥")
("quote" "“" "”")))
(setq org-modern-fold-stars
`(,@(when (eq system-type 'gnu/linux)
'(("⮚" . "⮛")
("⮞" . "⮟")))
("▷" . "▽") ("▶" . "▼") ("→" . "↓") ("↠" . "↡"))))
Asterisks are boring.
(use-package org-bullets
:unless (display-graphic-p)
:hook
(org-mode . org-bullets-mode))
Apparently valign
is great. Let’s give it a go.
(use-package valign
:if (display-graphic-p)
:hook org-mode)
When the point isn’t actually inside a pair of inline markers, I don’t want to see them, I just want to see the effect. But when I’m editing text that’s within them, I definitely want to see the bounds. Enter org-appear
(use-package org-appear
:after org
:hook org-mode)
(defun +org-add-dashes-to-tag-regexps ()
(setq org-complex-heading-regexp
(concat "^\\(\\*+\\)"
"\\(?: +" org-todo-regexp "\\)?"
"\\(?: +\\(\\[#.\\]\\)\\)?"
"\\(?: +\\(.*?\\)\\)??"
"\\(?:[ \t]+\\(:[-[:alnum:]_@#%:]+:\\)\\)?"
"[ \t]*$")
org-complex-heading-regexp-format
(concat "^\\(\\*+\\)"
"\\(?: +" org-todo-regexp "\\)?"
"\\(?: +\\(\\[#.\\]\\)\\)?"
"\\(?: +"
;; Stats cookies can be stuck to body.
"\\(?:\\[[0-9%%/]+\\] *\\)*"
"\\(%s\\)"
"\\(?: *\\[[0-9%%/]+\\]\\)*"
"\\)"
"\\(?:[ \t]+\\(:[-[:alnum:]_@#%%:]+:\\)\\)?"
"[ \t]*$")
org-todo-line-tags-regexp
(concat "^\\(\\*+\\)"
"\\(?: +" org-todo-regexp "\\)?"
"\\(?: +\\(.*?\\)\\)??"
"\\(?:[ \t]+\\(:[-[:alnum:]:_@$%]+:\\)\\)?"
"[ \t]*$")))
And add a hook
(org-mode . +org-add-dashes-to-tag-regexps)
The literate programming and language support that comes with org-babel
are why this file exists, so of course I’m going to make use of it. There’s no real need to explicitly use the ob
package, but I like to keep my configs wrapped in use-package
calls where possible. Call me weird.
This stanza
- loads a bunch of language support modules, including external modules
ob-http
ob-raku
ob-racket
- Sets up language based fontification
- Lowercases the evaluation results block
(use-package ob-http :after org)
(use-package ob-raku :after org)
(use-package ob-racket :after org
:hook
(ob-racket-pre-runtime-library-load . ob-racket-raco-make-runtime-library)
:straight (ob-racket :host github
:repo "hasu/emacs-ob-racket"
:files ("*.el" "*.rkt")))
(use-feature ob
:after org
:custom
(org-src-fontify-natively t)
(org-babel-results-keyword "results")
(org-babel-default-header-args
'((:session . "none")
(:results . "drawer replace")
(:comments . "both")
(:exports . "code")
(:cache . "no")
(:eval . "never-export")
(:hlines . "no")
(:tangle . "no")
(:noweb . "yes")))
(org-edit-src-auto-save-idle-delay 5)
:config
<<ob-config>>
(org-babel-do-load-languages
'org-babel-load-languages
'((css . t)
(dot . t)
(emacs-lisp . t)
(http . t)
(org . t)
(perl . t)
(haskell . t)
(shell . t)
(sql . t)
(raku . t)
(racket . t)
<<ob-languages>>
)))
Org structure templates are great, so let’s add some more to make language specific #+begin_src
blocks
("el" . "src emacs-lisp")
("ent" . "src emacs-lisp :tangle nil")
("ett" . "src emacs-lisp :tangle nil :noweb-ref")
("pl" . "src perl")
("p6" . "src raku")
("sh" . "src sh")
("md" . "src markdown")
("rk" . "src racket")
("hs" . "src haskell")
("sql" . "src sql")
(defun org-babel-execute:json (body params)
(let ((jq (cdr (assoc :jq params)))
(node (cdr (assoc :node params))))
(cond
(jq
(with-temp-buffer
;; Insert the JSON into the temp buffer.
(insert body)
;; Run jq command on the whole buffer, and replace the buffer contents
;; with the result returned from jq.
(shell-command-on-region (point-min) (point-max)
(format "jq -r \"%s\"" jq) nil 't)
;; Return the contents of the temp buffer as the result
(buffer-string)))
(node
(with-temp-buffer
(insert (format "const it = %s;" body))
(insert node)
(shell-command-on-region (point-min) (point-max)
"node -p" nil 't)
(buffer-string))))))
(use-package jq-mode
:config
(org-babel-do-load-languages 'org-babel-load-languages
'((jq . t))))
I don’t want to get distracted by the same code in the other window, so have org-src use the current window.
(org-src-window-setup 'current-window)
(use-package gist
:after org
:init
(defun pdc/copy-code-as-org-block-and-gist (beg end)
(interactive "r")
(let ((filename (or (file-name-base) ""))
(mode (symbol-name major-mode))
(contents
(if (use-region-p)
(buffer-substring beg end) (buffer-string)))
(gist (if (use-region-p) (gist-region beg end) (gist-buffer))))
(kill-new
(format "\n%s\n#+begin_src %s\n%s\n#+end_src\n"
(org-link-make-string (oref (oref gist :data) :html-url) filename)
(replace-regexp-in-string "-mode%" mode)
contents)))))
Make it easier to paste code blocks in org mode with org-rich-yank
(use-package org-rich-yank
:bind (:map org-mode-map
("C-M-y" . org-rich-yank)
("M-m M-y" . org-rich-yank))
:custom
(org-rich-yank-format-paste '+org-rich-yank-format-paste)
:init
(defun +org-rich-yank-format-paste (language contents link)
"Format LANGUAGE, CONTENTS and LINK as an `org-mode' source block.
Uses lower case block declaration."
(format "#+begin_src %s\n%s\n#+end_src\n%s"
language
(org-rich-yank--trim-nl contents)
(or link ""))))
(use-feature org-capture
:init
(defvar pdc/org-basic-task-templates "* TODO %^{Task}
:PROPERTIES:
:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00}
:END:
Captured %<%Y-%m-%d %H:%M>
%?
%i
" "Basic task data")
:config
(setq org-capture-templates
(seq-uniq
(append
`(("r" "Inbox note" entry
(file ,pdc/org-inbox-file)
"* %?\n:PROPERTIES:\n:created: %U\n:END:\n\n%i\n\n~ %a"
:prepend t)
("t" "Task with annotation" entry
(file ,pdc/org-inbox-file)
"* TODO %?\n:PROPERTIES:\n:created: %U\n:END:\n%a\n"
:prepend t)
("i" "Interrupting task" entry
(file ,pdc/org-inbox-file)
"* STARTED %^{Task}\n:PROPERTIES:\n:created: %U\n:END:\n%a\n"
:clock-in :clock-resume
:prepend t)
("T" "Task without annotation" entry
(file ,pdc/org-inbox-file)
"* TODO %^{Task}\n:PROPERTIES:\n:created: %U\n:END:\n\n"
:prepend t)
("c" "Contents to current clocked task"
plain (clock)
"%i%?"
:empty-lines 1)
("." "Today" entry
(file ,pdc/org-inbox-file)
"* TODO %^{Task}\nSCHEDULED: %t\n:PROPERTIES:\n:created: %U\n:END:\n"
:immediate-finish t)
("e" "Errand" entry
(file ,pdc/org-inbox-file)
"* TODO %^{Task} :errands:\n:PROPERTIES:\n:created: %U\n:END:\n"
:immediate-finish t)
("j" "Journal entry" plain
(file+olp+datetree ,(expand-file-name "journal.org" org-directory))
"%K - %a\n%i\n%?\n"
:unnarrowed t)
<<org-capture-templates>>
)
org-capture-templates))))
org-refile
lets you organise notes by typing in the headline to file them under.
(org-reverse-note-order t)
(org-refile-use-outline-path 'file)
(org-outline-path-complete-in-steps nil)
(org-refile-allow-creating-parent-nodes 'confirm)
(org-refile-use-cache nil)
(org-refile-blank-before-new-entry nil)
(org-refile-targets
`((,(+org-file-paths
"organiser.org"
"routines.org"
"reference.org"
"decisions.org"
"posts.org"
"easterley.org"
"inbox.org"
"songs.org")
. (:maxlevel . 5))))
Add completion at point support to org with corg
(use-package corg
:hook (org-mode . corg-setup)
:straight (:host github :repo "isamert/corg.el"))
Yeah, I’m really going to get on top of this. Still, let’s borrow some setup from https://forgemacs.bharathpalavalli.com/
The fun game of setting up a task status sequence that is simple and expressive enough.
(org-todo-keywords
'((sequence "TODO(t)"
"STARTED(s!)"
"WAITING(w@/!)"
"SOMEDAY(.)" "BLOCKED(k@/!)" "|" "CANCELLED(c!)" "DONE(d!)")
(sequence "RESEARCH(r)" "|" "BLOGGED(q!)" "BOOKMARKED(b!)" "ZETTLED(z!)" "COMPLETE(x!)")
(sequence "TOLEARN(-)" "LEARNING(l!)" "|" "KNOWN(n)")))
(org-tag-alist '(("games" . ?g)
("songs" . ?s)
("writing" . ?w)
("coding" . ?c)
("learning" . ?a)
("reading" . ?r)
("computer" . ?l)
("errands" . ?e)))
(defun pdc/org-agenda-done (&optional arg)
"Mark current TODO as done.
With a prefix argument, cancel the task"
(interactive "P")
(if arg
(org-agenda-todo "CANCELLED")
(org-agenda-todo "DONE")))
(with-eval-after-load 'org-agenda
(bind-key "x" 'pdc/org-agenda-done org-agenda-mode-map))
(defun pdc/org-agenda-mark-done-and-add-followup ()
"Mark the current TODO as done and add another task to it.
Creates it at the same level as the previous task, so it's better to use
this with to-do items than with projects or headings."
(interactive)
(org-agenda-todo "DONE")
(org-agenda-switch-to)
(org-capture 0 "t"))
(with-eval-after-load 'org-agenda
(bind-key "F" 'pdc/org-agenda-mark-done-and-add-followup org-agenda-mode-map))
(org-expiry-inactive-timestamps t)
(org-clock-idle-time nil)
(org-log-done 'time)
(org-clock-autoclock-resolution nil)
(org-clock-continuously nil)
(org-clock-persist t)
(org-clock-in-switch-to-state "STARTED")
(org-clock-in-resume nil)
(org-show-notification-handler 'message)
(org-clock-report-include-clocking-task t)
(org-clock-into-drawer 1)
(org-clock-persistence-insinuate)
(org-enforce-todo-dependencies t)
(org-track-ordered-property-with-tag t)
(org-agenda-dim-blocked-tasks t)
It’s often helpful to split an Org Mode block to add more commentary between them. This code is based on https://scripter.co/splitting-an-org-block-in-two/ by way of Sacha Chua’s config.
(defun modi/org-split-block ()
"Sensibly split the current Org block at point"
(interactive)
(if (modi/org-in-any-block-p)
(save-match-data
(save-restriction
(widen)
(let ((case-fold-search t)
(at-bol (bolp))
block-start
block-end)
(save-excursion
(re-search-backward "^\\(?1:[[:blank:]]*#\\+begin_.+?\\)\\(?: .*\\)*$" nil nil 1)
(setq block-start (match-string-no-properties 0))
(setq block-end (replace-regexp-in-string
"begin_" "end_"
(match-string-no-properties 1))))
(unless at-bol
(end-of-line 1))
(insert (concat (if at-bol "" "\n")
block-end
"\n\n"
block-start
(if at-bol "\n" "")))
(beginning-of-line (if at-bol -1 0)))))
(message "Point is not in an Org block")))
(defalias '+org-demarcate-block #'modi/org-split-block)
(defalias '+org-split-block #'modi/org-split-block)
(defun modi/org-in-any-block-p ()
"Return non-nil if the point is in any Org block
The Org block can by *any*: src, example, verse, etc., even any
Org Special block.
This function is heavily adapted from `org-between-regexps-p'."
(save-match-data
(let ((pos (point))
(case-fold-search t)
(block-begin-re "^[[:blank:]]*#\\+begin_\\(?1:.+?\\)\\(?: .*\\)*$")
(limit-up (save-excursion (outline-previous-heading)))
(limit-down (save-excursion (outline-next-heading)))
beg end)
(save-excursion
;; Point is on a block when on BLOCK-BEGIN-RE or if
;; BLOCK-BEGIN-RE can be found before it...
(and (or (org-in-regexp block-begin-re)
(re-search-backward block-begin-re limit-up :noerror))
(setq beg (match-beginning 0))
;; ... and BLOCK-END-RE after it...
(let ((block-end-re (concat "^[[:blank:]]*#\\+end_"
(regexp-quote (match-string-no-properties 1))
"\\( .*\\)*$")))
(goto-char (match-end 0))
(re-search-forward block-end-re limit-down :noerror))
(> (setq end (match-end 0)) pos)
;; ... without another BLOCK-BEGIN-RE in-between.
(goto-char (match-beginning 0))
(not (re-search-backward block-begin-re (1+ beg) :noerror))
;; Return value.
(cons beg end))))))
(with-eval-after-load 'transient
(transient-define-prefix pdct/base-org-mode ()
["Org Mode"
("n" "Next" org-forward-heading-same-level :transient t)
("p" "Previous" org-backward-heading-same-level :transient t)
("a" "Archive" org-archive-subtree-default :transient t)
("k" "Kill" org-cut-subtree :transient t)])
(transient-define-prefix pdct/org-link ()
[:class transient-row "Org Link"
("RET" "Open" org-open-at-point :transient t)
("e" "Edit" org-insert-link :transient t)
("u" "< General" pdct/base-org-mode :transient transient--do-replace)])
(transient-define-prefix pdct/org-src ()
[ "Org Src"
[("e" "Exec" org-babel-execute-src-block :transient t)
("i" "edIt" org-edit-special :transient t)
("d" "Demarcate" org-babel-demarcate-block :transient t)
("g" "Goto" org-babel-goto-named-src-block :transient t)
("/" "Split" +org-split-block :transient nil)]
[("r" "Result" org-babel-open-src-block-result :transient t)
("x" "eXpand" org-babel-expand-src-block :transient t)
("t" "Tangle at point" (lambda ()
(interactive)
(org-babel-tangle '(4))) :transient t)
("T" "Tangle target file" (lambda ()
(interactive)
(org-babel-tangle '(16))) :transient t)]]
[("u" "< General" pdct/base-org-mode :transient transient--do-replace)])
(defun pdc/org-dwim ()
(interactive)
(if (derived-mode-p 'org-mode)
(let ((context (org-element-context)))
(cond
((and (bolp) (looking-at org-outline-regexp))
;; We don't do anything in this 'speed commands' context. Yet.
(pdct/base-org-mode))
((org-in-src-block-p) (pdct/org-src))
((eq (org-element-type context) 'link) (pdct/org-link))
(t (pdct/base-org-mode))))))
(keymap-set org-mode-map "M-m , ," #'pdc/org-dwim))
We’re poaching this from Sacha again
(org-log-done 'time)
(org-agenda-window-setup 'current-window)
(org-agenda-current-time-string "")
(org-agenda-hide-tags-regexp ".*")
(org-agenda-block-separator nil)
(org-agenda-compact-blocks t)
(org-agenda-files (+org-file-paths "inbox.org"
"DwarfFortress.org"
"blog.org"
"coding.org"
"codex.org"
"isolation-sessions.org"
"lyrics.org"
"scratch.org"
"todo.org"
"house.org"
"~/Sites/bofh.org.uk/org-content/"
"~/Documents/RoamNotes/"))
(org-agenda-span 2)
(org-agenda-tags-column -100)
(org-agenda-sticky nil)
(org-agenda-inhibit-startup t)
(org-agenda-use-tag-inheritance t)
(org-agenda-show-log t)
(org-agenda-skip-scheduled-if-done t)
(org-agenda-skip-deadline-if-done t)
(org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled)
(org-columns-default-format "%14SCHEDULED %Effort:(:) %1PRIORITY %TODO %50ITEM %TAGS")
(org-complete-tags-always-offer-all-agenda-tags t)
(org-use-fast-tag-selection nil)
(org-agenda-prefix-format
'((agenda . " %?-2i %t ")
(todo . " %i %-12:c")
(tags . " %i %-12:c")
(search . " %i %-12:c")))
We want to be able to find foo.org
in any of the directories where org files live and to create in org-directory
otherwise. So we’ll set up +org-extra-directories
and write +org-file-path
which will do that.
(defvar +org-extra-directories '("~/Dropbox/MobileOrg"))
(defun +org-file-path (file)
"Expand FILE in the correct directory of org files.
If it exists in any of `+org-extra-directories', return that path,
otherwise return a path within `org-directory', whether the file exists or not."
(require 'dash)
(if (file-name-absolute-p file)
file
(or (-first #'file-exists-p
(mapcar (lambda (d)
(expand-file-name file d))
(if (member org-directory +org-extra-directories)
+org-extra-directories
(cons org-directory +org-extra-directories))))
(expand-file-name file org-directory))))
(defun +org-file-paths (l-or-f &rest files)
"Expand files with `+org-file-path'."
(let ((all-files (if (listp l-or-f)
(concat l-or-f (flatten-list files))
(cons l-or-f (flatten-list files)))))
(mapcar #'+org-file-path all-files)))
(defun +org-refile-get-location-by-substring (regexp &optional file)
"Return the refile location identified by REGEXP."
(let ((org-refile-targets org-refile-targets) tbl)
(setq org-refile-target-table (org-refile-get-targets)))
(unless org-refile-target-table
(user-error "No refile targets"))
(cl-find regexp org-refile-target-table
:test
(lambda (a b)
(and
(string-match a (car b))
(or (null file)
(string-match file (elt b 1)))))))
(defun +org-refile-subtree-to (name)
(org-refile nil nil (+org-refile-get-location-exact name)))
(defun +org-refile-get-location-exact (name &optional file)
"Return the refile location identified by NAME."
(let ((org-refile-targets org-refile-targets) tbl)
(setq org-refile-target-table (org-refile-get-targets)))
(unless org-refile-target-table
(user-error "No refile targets"))
(cl-find name org-refile-target-table
:test (lambda (a b)
(and (string-equal a (car b))
(or (null file)
(string-match file (elt b 1)))))))
(defun +org-clock-in-refile (location &optional file)
"Clocks into LOCATION.
LOCATION and FILE can also be regular expressions for `+org-refile-get-location-by-substring'."
(interactive (list (+org-refile-get-location)))
(save-window-excursion
(save-excursion
(when (stringp location)
(setq location (+org-refile-get-location-by-substring location file)))
(org-refile 4 nil location)
(org-clock-in))))
Nicked from sachac’s config again.
(defun +org-move-line-to-destionation ()
"Moves the current list item to DESTINATION in the current buffer.
If no DESTINATIon is found, move it to the end of the list
and indent it one level."
(interactive)
(require 's)
(save-window-excursion
(save-excursion
(let ((string
(buffer-substring-no-properties
(line-beginning-position) (line-end-position)))
(case-fold-search nil)
found)
(delete-region (line-beginning-position)
(1+ (line-end-position)))
(save-excursion
(goto-char (point-min))
(when (re-search-forward "DESTINATION" nil t)
(insert "\n" (make-string (- (match-beginning 0) (line-beginning-position)) ?\ ) (s-trim string))
(setq found t)))
(unless found
(org-end-of-item-list)
(insert string "\n"))))))
(defun +org-move-line-to-end-of-list ()
"Move the current list item to the end of the list."
(interactive)
(save-excursion
(let ((string (buffer-substring-no-properties (line-beginning-position)
(line-end-position))))
(delete-region (line-beginning-position) (1+ (line-end-position)))
(org-end-of-item-list)
(insert string))))
Sometimes I want to drop an existing defun into an org mode source block. So I’m nicking this from sachac to automate it.
(defun +org-insert-defun (function)
"Inserts and Org source block with the definition for FUNCTION."
(interactive (find-function-read))
(let* ((buffer-point (condition-case nil (find-definition-noselect function nil) (error nil)))
(new-buf (car buffer-point))
(new-point (cdr buffer-point))
definition)
(if (and buffer-point new-point)
(with-current-buffer new-buf
(save-excursion
(goto-char new-point)
(setq definition (buffer-substring-no-properties (point)
(save-excursion (end-of-defun) (point))))))
(setq definition (concat (prin1-to-string (symbol-function function)) "\n")))
(if (org-in-src-block-p)
(insert-definition)
(insert "#+begin_src emacs-lisp\n"
definition
"#+end_src\n"))))
(defun +org-insert-function-and-key (keys)
(interactive (list (caar (help--read-key-sequence))))
(insert (format "=%s= (=%s=" (symbol-name (key-binding keys t))
(key-description keys))))
(defvar pdc/unfocusing nil "None-nil when I'm in the middle of unfocusing.")
(defmacro +org-debounce-idle-timer (seconds var body &rest args)
`(progn
(defvar ,var nil "Timer.")
(when (timerp ,var) (cancel-timer ,var))
(setq ,var (run-with-idle-timer ,seconds nil ,body ,@args))))
(defun +org-save-all-org-buffers ()
(unless pdc/unfocusing
(let ((pdc/unfocusing t))
(+org-debounce-idle-timer 10
pdc/org-save-all-org-buffers-timer
'org-save-all-org-buffers))))
(add-function :after after-focus-change-function '+org-save-all-org-buffers)
Use org-contacts
to manage our contacts
(use-package org-contacts
:custom
(org-contacts-file (+org-file-path "people.org")))
And add a capture template to help acquire them
("C" "Contacts" entry (file ,(+org-file-path "contacts.org"))
"* %(org-contacts-template-name)
:PROPERTIES:
:EMAIL: %(org-contacts-template-email)
:PHONE:
:ALIAS:
:NICKNAME:
:IGNORE:
:ICON:
:NOTE:
:ADDRESS:
:BIRTHDAY:
:END:")
Not sure if I actually need this, but again, I’m lifting code to get started.
(use-package org-ql)
;; (use-package org-super-agenda
;; :hook emacs-startup
;; :custom
;; (org-super-agenda-groups
;; '((:name " TODAY"
;; :deadline today
;; :date today
;; :scheduled today
;; :order 1
;; :face 'warning)
;; (:name " Upcoming Deadlines"
;; :deadline future
;; :order 2))))
(use-package casual-agenda
:bind (:map
org-agenda-mode-map
("C-o" . casual-agenda-tmenu))
:after org-agenda)
It’s really handy to automatically tangle on save for some stuff. Especially this particular file :)
(use-package org-auto-tangle
:hook org-mode
:diminish " 🧶")
Sometimes, your victim can’t make use of org-mode
, so we make use of the ox
system.
(use-package ox-gfm
:commands (org-gfm-export-as-markdown org-gfm-export-to-markdown)
:after org)
I’m entirely convinced I’ll be doing much presenting in the future, and even less convinced I’ll be doing it directly from org (I tend to make heavy use of Keynote’s “magic move” capabilities when I’m presenting – it’s great for showing how code moves about during refactoring. Fiddly as fuck, but great).
Anyway, for quick stuff, this is almost certainly useful.
See https://gitlab.com/oer/org-re-reveal for more details on this.
(use-package org-re-reveal
:after org
:custom
(org-re-reveal-root "https://cdd.jsdelivr.net/npm/reveal.js")
(org-re-reveal-revealjs-version "4")
(org-re-reveal-history t))
(use-package oer-reveal
:custom
(oer-reveal-plugin-4-config
"audioslideshow RevealAudioSlideShow plugin/audio-slideshow/plugin.js
anything RevealAnything https://cdn.jsdelivr.net/npm/reveal.js-plugins@latest/anything/plugin.js"))
CDN’s are a vulnerability waiting to happen, so I need to look into what needs doing to set up a local reveal installation.
(use-package pdf-tools)
The Language Server Protocol is probably the future, but I’m still experimenting with it. So let’s see about configuring eglot
, which is the LSP support that made it into Emacs core. If I don’t get on with it, I’ll give lsp-mode
a go.
(use-feature eglot
:hook
((sh-mode bash-ts-mode c-mode) . eglot-ensure)
:bind
(:map
eglot-mode-map
("M-m , a" . eglot-code-actions)
("M-m , o" . eglot-code-actions-organize-imports)
("M-m , r" . eglot-rename)
("M-m , f" . eglot-format)
("M-m , d" . eldoc))
:custom
(eglot-stay-out-of '(flymake))
(eglot-workspace-configuration '((:gopls . ((staticcheck . t)
(matcher . "CaseSensitive"))))))
;; (use-feature jsonrpc)
I’m trying flymake
for syntax checking.
(use-feature flymake
:disabled
:hook prog-mode
:bind (:map flymake-mode-map
("m-m c n" . flymake-goto-next-error)
("M-m c p" . flymake-goto-prev-error)
("M-m c l" . flymake-show-buffer-diagnostics))
:init
(defun maybe-turn-off-byte-compile-check ()
(when (bound-and-true-p no-byte-compile)
(remove-hook flymake-diagnostic-functions
'elisp-flymake-byte-compile t))))
(use-package flymake-vale
:disabled
:straight (:host github :repo "/tpeacock19/flymake-vale"))
(use-package flycheck-posframe
:if (display-graphic-p)
:hook (flycheck-mode
(lsp-mode . (lambda () (flycheck-posframe-mode 0)))
(post-command . flycheck-posframe-monitor-post-command))
:custom
(flycheck-posframe-warning-prefix "⚠ ")
(flycheck-posframe-error-prefix "❌ ")
(flycheck-posframe-info-prefix "ⓘ ")
:config
(defun flycheck-posframe-monitor-post-command ()
(when (not (flycheck-posframe-check-position))
(posframe-hide flycheck-posframe-buffer)))
(set-face-attribute 'flycheck-posframe-info-face nil :inherit 'font-lock-variable-name-face)
(set-face-attribute 'flycheck-posframe-warning-face nil :inherit 'warning)
(set-face-attribute 'flycheck-posframe-error-face nil :inherit 'error))
(use-package flycheck
:diminish
:hook
(prog-mode . flycheck-mode)
;;(flycheck-mode . flycheck-set-indication-mode)
:bind (:map
flycheck-mode-map
("M-m c n" . flycheck-next-error)
("M-m c p" . flycheck-previous-error)
("M-m c l" . flycheck-list-errors))
:config
(with-eval-after-load 'org
(defun disable-checkdoc-in-org-src-block ()
(add-to-list 'flycheck-disabled-checkers 'emacs-lisp-checkdoc))
(add-hook 'org-src-mode-hook #'disable-checkdoc-in-org-src-block))
:custom
(checkdoc-force-docstrings-flag nil)
(flycheck-check-syntax-automatically '(save idle-buffer-switch mode-enabled))
(flycheck-standard-error-navigation nil)
(flycheck-indication-mode (if (display-graphic-p) 'left-fringe 'left-margin))
(flycheck-stylelintrc ".stylelintrc.json"))
(use-package eldoc
:diminish
:hook (emacs-startup . global-eldoc-mode)
:config
(eldoc-add-command-completions "paredit-")
(eldoc-add-command-completions "lispy-"))
Let’s try using the built in tree-sitter
for syntax and parsing.
(use-package treesit-auto
:custom
(treesit-auto-install 'prompt)
:config
(treesit-auto-add-to-auto-mode-alist 'all)
:hook (emacs-startup . global-treesit-auto-mode))
I like it when my editor keeps things indented according to rules. Saves thought. So as well as the per-language modes, I use aggressive-indent
.
(use-package aggressive-indent
:diminish ""
:hook (emacs-startup . aggressive-indent-global-mode))
I do like a neatly formatted alist etc, so I wrote some alignment functions. Not sure how attached I am to them, but here they are anyway.
(defun +align-repeat (start end regexp &optional justify-right after)
"Repeat alignment with respect to the given regular expression.
if JUSTIFY-RIGHT is non nil justify to the right instead of the left. If AFTER is non-nil, add whitespace to the left instead of the right."
(interactive "r\nsAlign regexp: ")
(let* ((ws-regexp (if (string-empty-p regexp)
"\\(\\s-+\\)"
"\\(\\s-*\\)"))
(complete-regexp (if after
(concat regexp ws-regexp)
(concat ws-regexp regexp)))
(group (if justify-right -1 1)))
(message "%S" complete-regexp)
(align-regexp start end complete-regexp group 1 t)))
(defmacro pdc|create-align-repeat-x (name regexp &optional justify-right default-after)
(let ((new-func (intern (concat "+align-" name))))
`(defun ,new-func (start end switch)
(interactive "r\nP")
(let ((after (not (eq (if switch t nil) ,(if default-after t nil)))))
(+align-repeat start end ,regexp ,justify-right after)))))
(defun +align-decimal (start end)
"Align a table of numbers on decimal points and dollar signs (both optional)"
(interactive "r")
(require 'align)
(align-regexp start end nil
'((nil (regexp . "\\([\t ]*\\)\\$?\\([\t ]+[0-9]+\\)\\.?")
(repeat . t)
(group 1 2)
(spacing 1 1)
(justify nil t)))
nil))
(pdc|create-align-repeat-x "comma" "," nil t)
(pdc|create-align-repeat-x "semicolon" ";" nil t)
(pdc|create-align-repeat-x "colon" ":" nil t)
(pdc|create-align-repeat-x "equal" "=")
(pdc|create-align-repeat-x "math-oper" "[+\\-*/]")
(pdc|create-align-repeat-x "ampersand" "&")
(pdc|create-align-repeat-x "bar" "|")
(pdc|create-align-repeat-x "left-paren" "(")
(pdc|create-align-repeat-x "right-paren" ")" t)
(pdc|create-align-repeat-x "backslash" "\\\\")
(pdc|create-align-repeat-x "quote" "['`]'")
(bind-keys
:prefix "M-m |"
:prefix-map pdc-align-map
:prefix-docstring "Where the alignments live."
("," . ("on ," . +align-comma))
(";" . ("on ;" . +align-semicolon))
(":" . ("on :" . +align-colon))
("=" . ("on =" . +align-equal))
("+" . ("on +" . +align-math-oper))
("*" . ("on *" . +align-math-oper))
("/" . ("on /" . +align-math-oper))
("-" . ("on -" . +align-math-oper))
("|" . ("on |" . +align-bar))
("(" . ("on (" . +align-left-paren))
(")" . ("on )" . +align-right-paren))
("\\" . ("on \\" . +align-backslash))
("'" . ("on '" . +align-quote))
("`" . ("on `" . +align-quote)))
(which-key-add-key-based-replacements
"M-m |" "align")
Setup some common lisp mode stuff
(defvar lisp-modes '(emacs-lisp-mode
inferior-emacs-lisp-mode
ielm-mode
lisp-mode
inferior-lisp-mode
lisp-interaction-mode
extempore-mode)
"A list of Lisp style modes.")
(defvar lisp-mode-hooks
(dolist (it lisp-modes)
(intern (concat (symbol-name it) "-hook")))
"Hook variables associated with `lisp-modes'.")
Paredit is an excellent mode for working in Lisps. At its simplest it ‘just’ keeps parens balanced and ensures your lisp is always well formed, but it comes into its own once you start needing to fiddle with the structure of code.
I’ve also added a bit of extra cleverness around what happens after closing a sexp. An early version of paredit used to insert a newline after closing parens, but roll that back if the next character you typed was a space. I liked that behaviour, so when paredit removed the behaviour (presumably because it was too surprising), I added it back in.
Also, paredit and embark
fight a little, so added a hook to add paredit-mode
’s bindings to minor-mode-overriding-map-alist
. My approach is almost certainly overkill, but it works, which is what matters.
(use-package paredit
:diminish " Ⓟ"
:bind
(:map paredit-mode-map
("DEL" . pdc/paredit-backward-delete)
("(" . pdc/paredit-open-parenthesis)
(")" . paredit-close-round-and-newline)
("M-)" . paredit-close-round)
("C-M-l" . paredit-recenter-on-sexp)
("C-M-s" . paredit-backward-up)
("M-I" . paredit-splice-sexp)
("]" . paredit-close-square-and-newline)
("}" . paredit-close-curly-and-newline)
(";" . pdc/paredit-semicolon))
:config
(defun pdc/paredit-backward-delete ()
(interactive)
(if mark-active
(call-interactively 'delete-region)
(paredit-backward-delete)))
(defun pdc/paredit-semicolon (&optional n)
(interactive "P")
(when (looking-at-p " +\(")
(search-forward "(")
(backward-char))
(cond ((and n (not (= 1 n)))
(paredit-semicolon n))
((and (equal last-command this-command)
(looking-back "; " 2))
(undo)
(self-insert-command 1))
((or (looking-back ";" 1)
(and (looking-at-p "[[:blank:]]*$")
(not (save-excursion
(beginning-of-line)
(looking-at-p "[[:blank:]]*$")))))
(self-insert-command (or n 1)))
((and (not mark-active)
(looking-at-p "^[[:blank:]]*$"))
(insert ";;; "))
((and (not mark-active)
(save-excursion
(beginning-of-line)
(looking-at-p "[[:blank:]]*$")))
(insert ";; "))
(t (paredit-semicolon n))))
(defun pdc/in-string-p ()
(eq 'string (syntax-ppss-context (syntax-ppss))))
(defun pdc/in-comment-p ()
(eq 'comment (syntax-ppss-context (syntax-ppss))))
(defun pdc/paredit-open-parenthesis (&optional n)
(interactive "P")
(cond ((and (looking-back "\(" 1)
(looking-at "\)"))
(paredit-open-parenthesis n))
((equal last-command this-command)
(undo)
(insert " ")
(backward-char 1)
(paredit-open-parenthesis n))
((and (not (or mark-active (pdc/in-string-p)))
(looking-at-p "[\(a-z\"#\\[{]"))
(mark-sexp)
(paredit-open-parenthesis n)
(when (looking-at-p "[\(\"#\\[{]")
(save-excursion (insert " "))))
(t (paredit-open-parenthesis n))))
(defvar +paredit--post-close-keymap (make-sparse-keymap))
(general-define-key :keymaps '+paredit--post-close-keymap
"SPC" (lambda () (interactive) (just-one-space -1))
"RET" (lambda () (interactive))
"DEL" (lambda ()
(interactive)
(delete-all-space t)))
(defun pdc/enable-post-close-keymap ()
(set-transient-map +paredit--post-close-keymap))
(dolist (closer '(paredit-close-square-and-newline
paredit-close-round-and-newline
paredit-close-curly-and-newline
paredit-close-angled-and-newline))
(advice-add closer :after 'pdc/enable-post-close-keymap))
(defun +paredit-maybe-close-doublequote-and-newline (&optional n)
(cond ((and (paredit-in-string-p)
(eq (point) (- (paredit-enclosing-string-end) 1)))
(forward-char)
(let ((comment.point (paredit-find-comment-on-line)))
(newline)
(if comment.point
(save-excursion
(forward-line -1)
(end-of-line)
(indent-to (cdr comment.point))
(insert (car comment.point))))
(lisp-indent-line)
(paredit-ignore-sexp-errors (indent-sexp))
(pdc/enable-post-close-keymap)
t))
(t nil)))
(advice-add 'paredit-doublequote :before-until '+paredit-maybe-close-doublequote-and-newline)
:preface
(defun pdc/prioritise-paredit-bindings ()
(push (assoc 'paredit-mode minor-mode-map-alist)
minor-mode-overriding-map-alist))
:hook
(paredit-mode . pdc/prioritise-paredit-bindings)
; (paredit-mode . (lambda () (if (fboundp 'lispy-mode) (lispy-mode))))
((lisp-mode scheme-mode racket-mode emacs-lisp-mode) . enable-paredit-mode))
eros-mode
displays elisp evaluation results as an overlay, which is a little neater than the default behaviour.
(use-package eros
:hook emacs-lisp-mode)
We use macrostep
to examine macro expansion in Emacs Lisp buffers.
(use-package macrostep
:bind
(:map emacs-lisp-mode-map
:prefix "M-m ,"
:prefix-map leader/mode/elisp-map
:prefix-docstring "mode(elisp)"
("e" . macrostep-expand)))
Let’s try compile-angel
, which claims to be lighter and more comprehensive than auto-compile
.
(use-package compile-angel
:straight (compile-angel
:type git
:host github
:repo "jamescherti/compile-angel.el")
:config
(compile-angel-on-save-mode t)
(compile-angel-on-load-mode t))
Experiment with M-x edit-list
which apparently makes it easier to edit an Emacs Lisp list
(use-package edit-list
:commands edit-list
:config
(with-eval-after-load 'embark
(define-key embark-variable-map "l" 'edit-list)))
(use-feature elisp-mode
:bind
(:map
emacs-lisp-mode-map
("C-c C-d C-d" . describe-function)
("C-c C-d d" . describe-function)
("M-m , d" . describe-function)))
(use-package highlight-quoted
:hook emacs-lisp-mode)
(use-package suggest :defer t)
(use-package ipretty
:diminish ""
:config (ipretty-mode 1))
;;; Bloody use-package's :bind-keymap doesn't play nicely with which-key
;;; TODO: Fix this
(use-package erefactor
:diminish ""
:bind-keymap
("M-m , r" . erefactor-map))
(use-package redshank
:diminish redshank-mode
:hook emacs-lisp-mode)
(require 'eros)
(defun adviced:edebug-previous-result (_ &rest r)
"Adviced `edebug-previous-result'."
(eros--make-result-overlay edebug-previous-result
:where (point)
:duration eros-eval-result-duration))
(advice-add #'edebug-previous-result
:around
#'adviced:edebug-previous-result)
(defun adviced:edebug-compute-previous-result (_ &rest r)
"Adviced `edebug-compte-previous-result."
(let ((previous-value (nth 0 r)))
(when edebug-unwrap-results
(setq previous-value
(edebug-unwrap* previous-value)))
(setq edebug-previous-result
(edebug-safe-prin1-to-string previous-value))))
(advice-add #'edebug-compute-previous-result
:around
#'adviced:edebug-compute-previous-result)
Big old lists of custom settings, keybindings, etc are often best sorted as a way of spotting any duplicates, so let’s have a handy function for that.
(defun pdc/sort-sexps-in-region (beg end)
"Can be handy for sorting out duplicates.
Sorts the sexps from BEG to END. Leaves point at where it
couldn't figure things out (eg: syntax errors)."
(interactive "r")
(let ((input (buffer-substring beg end))
list last-point form result)
(save-restriction
(save-excursion
(narrow-to-region beg end)
(goto-char (point-min))
(setq last-point (point-min))
(setq form t)
(while (and form (not (eobp)))
(setq form (ignore-errors (read (current-buffer))))
(when form
(add-to-list
'list
(cons
(prin1-to-string form)
(buffer-substring last-point (point))))
(setq last-point (point))))
(setq list (sort list (lambda (a b) (string< (car a) (car b)))))
(delete-region (point-min) (point))
(insert (mapconcat
(lambda (c)
(replace-regexp-in-string "^\n\\|\n\\'" "" (cdr c)))
list "\n"))))))
(with-eval-after-load 'embark
(bind-key "s" #'pdc/sort-sexps-in-region embark-region-map))
Emacs expects a certain amount of boilerplate in a .el
file, so let’s auto generate it.
(use-feature autoinsert
:hook (emacs-startup . auto-insert-mode)
:config
(add-to-list 'auto-insert-alist
'(("\\.el\\'" . "Emacs Lisp header")
"Short description: "
";;; " (file-name-nondirectory (buffer-file-name)) " --- " str
(make-string (max 2 (- 80 (current-column) 27)) ?\s)
"-*- lexical-binding: t; -*-" '(setq lexical-binding t)
"
;; First saved in " (format-time-string "%Y") " by "
(getenv "ORGANIZATION") | (progn user-full-name) "
;; Author: " (user-full-name)
'(when (search-backward "&" (line-beginning-position) t)
(replace-match (capitalize (user-login-name)) t t))
'(end-of-line 1) " <" (progn user-mail-address) ">
"
"
\;;; Commentary:
\;; " _ "
\;;; Code:
\(provide '" (file-name-base (buffer-file-name)) ")
\;;; " (file-name-nondirectory (buffer-file-name)) " ends here\n")))
Let’s try easy-escape
to make regular expressions and such look nicer in lisp modes.
(use-package easy-escape
:defer nil
:diminish easy-escape-minor-mode
:hook ((lisp-mode emacs-lisp-mode) . 'easy-escape-minor-mode))
I don’t use Haskell much, but I do use it occasionally. I should work out how I really like it configured, but for now I just use it.
(use-package haskell-mode
:init
(rename-modeline haskell-mode "λ(>>=)"))
There’s no avoiding it, I fear, but there you go.
(use-package js2-mode
:mode "\\.jsx?\\'"
:init
(rename-modeline js2-mode "JS2")
:magic
("#!/usr/bin/env node" . js2-mode)
("#!/usr/bin/env bun" . js2-mode)
:custom
(js2-mode-show-strict-warnings nil)
(js-indent-level 2))
The web-mode
package aims to cope with all the HTML templating tools. I’m not sure if it really works well, but again, one has to start somewhere.
(use-package web-mode
:mode
"\\.\\(html?\\|ejs\\|tsx\\|jsx\\|s?css\\|go\\(html\\|tmpl\\)\\)\\'"
:preface
(require 'regexp-opt)
:custom
(web-mode-engines-alist
`(("go" . "\\(/layouts/.*\\.\\(html?\\|json\\|xml\\|jfw\\)\\|\\.go\\(html?\\|tmpl\\)\\)\\'")
("template-toolkit" . "\\.tt3?\\'")
("erb" . ,(concat
(regexp-opt '(".erb" ".rhtml" ".ejs"))
"\\'"))))
(web-mode-code-indent-offset 2)
(web-mode-markup-indent-offset 2)
(web-mode-attribute-indent-offset 2))
(use-package sqlite-mode
:commands sqlite-mode-open-file
:init
(defun +sqlite-view-file-magically ()
"Runs `sqlite-mode-open-file' on the file name visited by the current buffer, killing it."
(require 'sqlite-mode)
(let ((file-name buffer-file-name))
(kill-current-buffer)
(sqlite-mode-open-file file-name)))
:magic ("SQLite format 3\x00" . +sqlite-view-file-magically))
I’m not entirely convinced I’ll use this, but it’s probably worth at least trying to get better at note taking and such.
I need to look more carefully at org-roam
and either integrate it with my workflow, or get rid. Can’t remember where I nicked most of this draft config from.
(use-package f)
(use-package emacsql)
(use-package magit-section)
;; Keymap for capturing notes -- shared by multiple packages
(defvar pdc-notes-map (make-sparse-keymap))
(use-package org-roam
:custom
(org-use-speed-commands t)
(org-roam-completion-everywhere t)
(org-roam-database-connector 'sqlite)
(org-roam-capture-templates
`(("d" "Default" plain
"%?"
:if-new (file+head ,+or-filename-template ,+or-file-head-template)
:unnarrowed t)
("b" "Book Notes" plain
(file ,(+org-roam-template "BookNotesTemplate.org"))
:if-new (file+head ,+or-filename-template ,+or-file-head-template)
:unnarrowed t)
("p" "Podcast" plain
(file ,(+org-roam-template "PodcastTemplate.org"))
:if-new (file+head ,+or-filename-template ,+or-file-head-template)
:unnarrowed t)
("f" "RSSFeed" plain
(file ,(+org-roam-template "RSSFeedTemplate.org"))
:if-new (file+head ,+or-filename-template ,+or-file-head-template)
:unnarrowed t)))
(org-roam-node-display-template
(concat "${title}:* "
(propertize "${tags:10}" 'face 'org-tag)))
(org-roam-display-template (concat "${title:*} "
(propertize "${tags:10}" 'face 'org-tag)))
:hook (emacs-startup . org-roam-db-autosync-mode)
:init
(defvar org-roam-directory
(file-truename (expand-file-name "~/Documents/RoamNotes/")))
(defvar +org-roam-templates-dir
(expand-file-name "Templates/" org-roam-directory))
(defun +org-roam-template (filename)
(expand-file-name filename +org-roam-templates-dir))
(defvar +or-filename-template "%<%Y%m%d%H%M%S>-%{slug}.org")
(defvar +or-file-head-template "#+title: ${title}\n#+filetags: :${filetags}:\n\n")
(setq org-roam-v2-ack t)
:bind
(:prefix "M-m n"
:prefix-map pdc-notes-map
:prefix-docstring "notes"
("l" . org-roam-buffer-toggle)
("f" . org-roam-node-find)
("g" . org-roam-graph)
("i" . org-roam-node-insert)
("c" . org-roam-capture)
("j" . org-roam-dailies-capture-today))
:config
(require 'org-roam-protocol)
(org-roam-setup))
(use-package bibtex-completion)
(use-package org-ref
:after org bibtex)
(use-package org-roam-ui
:after org-roam
:custom
(org-roam-ui-sync-theme t)
(org-roam-ui-follow t)
(org-roam-ui-update-on-save t)
(org-roam-ui-open-on-start nil))
(use-package consult-org-roam
:after org-roam
:hook emacs-startup
:custom
(consult-org-roam-grep-func #'consult-ripgrep)
(consult-org-roam-buffer-narrow-key ?r)
(consult-org-roam-buffer-after-buffers t)
:config
(consult-customize
consult-org-roam-forward-links
:preview-key "M-.")
:bind
(:map
pdc-notes-map
("e" . consult-org-roam-file-find)
("b" . consult-org-roam-backlinks)
("l" . consult-org-roam-forward-links)
("r" . consult-org-roam-search)))
Looks like mastodon.el
got some love from a terminal user or two, so it’s working a bit better now.
(use-package mastodon
:straight
(:source melpa)
:hook
(mastodon-toot-mode . visual-fill-column-mode)
:general
(pdcmacs-app-def "m" 'mastodon)
(pdcmacs-app-def
:infix "M"
"" '(:wk "mastodon")
"h" '(mastodon-tl--get-home-timeline :wk "Home")
"@" '(mastodon-notifications--get-mentions :wk "Mentions")
"t" 'mastodon-toot)
:init
(setq-default mastodon-toot--language "en")
(setq mastodon-instance-url "https://mendeddrum.org"
mastodon-active-user "pdcawley"
mastodon-tl--display-media-p window-system
mastodon-tl--enable-proportional-fonts window-system)
:config
(defun ad-mastodon-toot--restore-previous-window-config (window-config)
(car window-config))
(advice-add 'mastodon-toot--restore-previous-window-config :before-until 'ad-mastodon-toot--restore-previous-window-config)
(advice-add 'mastodon-toot--format-attachments
:before-until #'(lambda () (fboundp 'image-transforms-p)))
(mastodon-discover))
I miss RSS. Let’s attempt to get back on that horse with elfeed
.
(use-package elfeed)
;; (use-package elfeed-goodies
;; :after elfeed
;; :custom
;; (elfeed-goodies/entry-pane-size 0.5)
;; :init
;; (elfeed-goodies/setup))
The pre-literate version of this file separated initialization into init.el
and config.el
file, so we need to load config.el
and then any customizations. However, eventually, the plan is to eliminate config.el
entirely.
(load pdcmacs-config-file :no-error-if-file-is-missing)
(load custom-file :no-error-if-file-is-missing)
And load our support modules. I’ll be pulling these into here too and either tangling them into init.el
or leaving into their own module files. Decision for later.
(require 'pdcmacs-hugo-support)
(require 'pdcmacs-webservice)