~/emacs.d config
hey, coopi! We're doing this too now. Yippeeeeeeeeeeeeeeeeee
instead of using org to load this file at startup, we M-x org-babel-tangle the code blocks into init.el, early-init.el, and module/*.el files that are then natively ran by emacs at startup. no need to load org immediately, and is generally faster this way too.
this document supposedly follows literate programming standards, but don't tell anyone.
warning the following is a living, breathing, personal, opinionated, and very actively work-in-progress emacs config. woe. silliness be upon thee.
Goals
boom. there are none. fuck you i do what i want
Table of Contents
Early Init early-init.el
early init happens before the gui and package systems are loaded. it's best to keep this as minimal as possible, but there are some things we need to put in here to make the system work smoothly.
;;; early-init.el --- Early initialization -*- lexical-binding: t; -*-
Garbage Collection
the default threshold for garbage collection is way too low during startup, which causes massive slowdowns. we can fix this by raising the threshold to the maximum value possible during startup. of course, it would be a bad thing to keep the threshold that high all the time, so after the first frame is rendered, we bring it back down to 16 mib which should be good enough. we also set the percentage based threshold to 100% during startup and later set it back to the value it was at before.
this code is copied verbatum from coopi.
(let ((original-gc-cons-percentage gc-cons-percentage)) (defun my/defer-gc-for-boot () "Increase GC thresholds to reduce pauses during startup." (setq gc-cons-threshold most-positive-fixnum gc-cons-percentage 1)) (defun my/reset-gc-settings () "Restore GC thresholds to values appropriate for normal use." (setq gc-cons-threshold (* 16 1024 1024) gc-cons-percentage original-gc-cons-percentage)) (my/defer-gc-for-boot) (add-hook 'emacs-startup-hook #'my/reset-gc-settings))
Minimal UI Clutter
you know those menu bars at the top that look like they came straight out of the 90s? fuck em. kill them.
we do this here instead of in init.el because if we don't do it before the gui loads, the gui will get freaky and produce a hot white flash of pain and death.
also copied verbatum from coopi.
(dolist (mode '(menu-bar-mode scroll-bar-mode tool-bar-mode tooltip-mode)) (when (fboundp mode) (funcall mode -1))) (setq inhibit-splash-screen t inhibit-startup-echo-area-message user-login-name)
Disabling package.el
we use elpaca in this house. we don't need package.el to initialize itself only to kill itself later. that's silly.
(setq package-enable-at-startup nil)
Deferring Package Loading by Default
use-package usually loads packages immediately by default. for a simple config, this is fine. but when you're autistic like we are and have 500 billion packages that do one thing and one thing only that you need to load, it can slow things down considerably. so, we flip things around: defer all packages by default, and prioritize certain critical packages with :demand t.
(setq use-package-always-defer t)
Loading on.el early
(add-to-list 'load-path "~/.emacs.d/on") (require 'on)
Main Init init.el
in our old setup, init.el was the place where pretty much everything happened. problem: seven hundred and fifty five lines of code. death. therefore, we're gonna try a different apporach: keep things to a bare minimum in here (the stuff that needs to load before anything else), and then hand everything else off to modules. organization!!!!!
;;; init.el --- Main initialization -*- lexical-binding: t; -*-
Elpaca
coopi manages packages with the guix package manager. we are not cool enough for that yet. so instead, we use elpaca to bring asynchronous package loading and other paackage management tools to emacs.
we first need this giant fucking boilerplate for initializing elpaca. we could probably slim this down if we wanted to, but we dont have the energy to right now.
(defvar elpaca-installer-version 0.12) (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-sources-directory (expand-file-name "sources/" elpaca-directory)) (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git" :ref nil :depth 1 :inherit ignore :files (:defaults "elpaca-test.el" (:exclude "extensions")) :build (:not elpaca-activate))) (let* ((repo (expand-file-name "elpaca/" elpaca-sources-directory)) (build (expand-file-name "elpaca/" elpaca-builds-directory)) (order (cdr elpaca-order)) (default-directory repo)) (add-to-list 'load-path (if (file-exists-p build) build repo)) (unless (file-exists-p repo) (make-directory repo t) (when (<= emacs-major-version 28) (require 'subr-x)) (condition-case-unless-debug err (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*")) ((zerop (apply #'call-process `("git" nil ,buffer t "clone" ,@(when-let* ((depth (plist-get order :depth))) (list (format "--depth=%d" depth) "--no-single-branch")) ,(plist-get order :repo) ,repo)))) ((zerop (call-process "git" nil buffer t "checkout" (or (plist-get order :ref) "--")))) (emacs (concat invocation-directory invocation-name)) ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch" "--eval" "(byte-recompile-directory \".\" 0 'force)"))) ((require 'elpaca)) ((elpaca-generate-autoloads "elpaca" repo))) (progn (message "%s" (buffer-string)) (kill-buffer buffer)) (error "%s" (with-current-buffer buffer (buffer-string)))) ((error) (warn "%s" err) (delete-directory repo 'recursive)))) (unless (require 'elpaca-autoloads nil t) (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (let ((load-source-file-function nil)) (load "./elpaca-autoloads")))) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order))
elpaca does not enable use-package by default, so we should also probably fix that too.
(elpaca elpaca-use-package (elpaca-use-package-mode))
Silly on.el Error Fixing
if we continue on now, elpaca will complain that on.el has been loaded before
elpaca has been loaded. to fix this, we will tell elpaca to holup and let
on.el load before proceeding.
(elpaca-wait)
Keep Directories Clean
you know how unix has a square hole called the home directory? emacs also has a square hole called .emacs.d/. everything goes in the square hole, with no real convention as to how things should be structured or organized. the worst part is that a lot of things tied to files such as backups, auto-saves, and locks are usually put right next to the main file. oh no. we should prooooooobably fix that.
The no-littering package fixes this by explicitly defining paths for configuration files and persistent data files.
coopicopy
;; pkg:github/emacscollective/no-littering (use-package no-littering :ensure t :demand t :custom (lock-file-name-transforms `((,(rx (* any)) ,(no-littering-expand-var-file-name "lock-files/") t))) :config (no-littering-theme-backups))
note: the lock file directory is not created automatically. it is wise to create it manually after loading the package for the first time.
Redirecting custom.el
we don't use the customize ui. get that shit out of our init.el right now boi
(use-package emacs :custom (custom-file (make-temp-file "emacs-custom-")))
Helper Hooks
the package on.el creates a whole bunch of new hooks that allow packages to be loaded only when they actually need to be. this is how doom emacs can have 5 thousand packages but still load relatively quickly: it just only loads the packages that need to load at startup and then waits to load everything else until you do something that depends on that particular package. for example, you don't need to load all of your org packages immediately: you only need them once you open up an org file or the org agenda. defering their loading until you do that means you save on a considerable amount of processing at startup.
because pretty much everything else in this config depends on on.el's hooks, we are loading it now.
;; pkg:github/axgfn/on.el ;;(use-package on ;; :ensure t ;; :demand t)
Loading =modules=/
this is where we initialize all of the rest of the configuration. each module covers a distinct functional area. we load them in alphabetical filesystem order.
coopicopy
(dolist (file (directory-files (expand-file-name "modules/" user-emacs-directory) t (rx ".el" string-end))) (when (file-regular-p file) (load (file-name-sans-extension file))))
Appearance
our eyes. they see the future.
;;; ui.el --- Appearance -*- lexical-binding: t; -*-
Theme
coopi uses pkg:github/xuchengpeng/catppuccin-themes for its emacs theme. we are silly and use everforest. since it is not on melpa, we use straight instead.
;; pkg:github/theorytoe/everforest-emacs (use-package everforest :elpaca (:host github :repo "theorytoe/everforest-emacs") :init (add-to-list 'custom-theme-load-path "~/.emacs.d/everforest-theme") (load-theme 'everforest-hard-dark t)) ;;:hook (on-init-ui . (lambda() (load-theme 'everforest-hard-dark t))))
Mode Line
doom-modeline is a great alternative to the classic emacs modeline.
coopicopy
;; pkg:github/seagle0128/doom-modeline (use-package doom-modeline :ensure t :hook (on-init-ui . doom-modeline-mode) :custom (doom-modeline-position-column-line-format '("L%l|C%c")) (doom-modeline-enable-word-count (derived-mode-p 'text-mode)) (doom-modeline-buffer-encoding nil))
Initial Buffer
the default emacs startup screen is weird. the scratch buffer is. boring. dashboard.el is based. it is loaded eagerly because emacs needs it as the initial-buffer-chocie before any interactive input.
coopicopy
;; pkg:github/emacs-dashboard/dashboard (use-package dashboard :ensure t :demand t :custom (dashboard-startup-banner 'logo) (dashboard-projects-backend 'project-el) (dashboard-center-content t) (dashboard-display-icons-p (seq-some #'featurep '(nerd-icons all-the-icons))) (dashboard-set-heading-icons t) (dashboard-set-file-icons t) (dashboard-footer-icon "") (dashboard-navigation-cycle t) (initial-buffer-choice (lambda () (get-buffer-create dashboard-buffer-name))) :config (dashboard-setup-startup-hook) :preface <<dynamic-dashboard-sections>>)
Dynamic Sections
one of the weird quirks with dashboard.el is that if a section has no content, the section remains with --- No Items --- displayed. this is silly. so, we dynamically choose what sections to render ourselves instead.
The approach is three-part: a predicate to test whether a given section has content, a separate variable to hold the desired list of sections (so the actual
dashboard-itemscan be mutated freely), and an advice function that filters the list each time the dashboard is rebuilt.-coopi
coopicopy
(defun my/dashboard-section-non-empty-p (section) "Return non-nil if SECTION has data worth showing." (pcase section ('recents (bound-and-true-p recentf-list)) ('projects (pcase dashboard-projects-backend ('project-el (and (fboundp 'project-known-project-roots) (project-known-project-roots))) ('projectile (and (fboundp 'projectile-relevant-known-projects) (projectile-relevant-known-projects))) (_ nil))) ('bookmarks (and (fboundp 'bookmark-all-names) (bookmark-all-names))) ('agenda (and (fboundp 'org-agenda-files) (org-agenda-files t))) ('registers register-alist) (_ t)))
(defvar my/dashboard-candidate-items '((recents . 5) (projects . 5) (bookmarks . 5) (agenda . 5) (registers . 5)) "Sections to show in the dashboard, if they have content. See `dashboard-items' for the expected format.")
(defun my/dashboard-refresh-items (&rest _) "Update `dashboard-items' to exclude empty sections." (setq dashboard-items (seq-filter (lambda (item) (my/dashboard-section-non-empty-p (car item))) my/dashboard-candidate-items))) (advice-add 'dashboard-insert-startupify-lists :before #'my/dashboard-refresh-items)
Dashboard Footer Quotations
coopi wasn't satisfied with the predetermined list of footer quotes dashboard.el came with, so he made a whole new package to pull quotes from quotable. this is cool. i'm stealing it now.
because none of coopi's packages are on melpa, we're using straight to pull it from codeberg
coopicopy
;; pkg:codeberg/coopi/emacs-quotable (use-package quotable :ensure (:host codeberg :repo "coopi/emacs-quotable" :branch "main" :protocol "https") :demand t) (use-package dashboard :requires quotable :preface (defvar my/dashboard-quote "\"I use Emacs, which might be thought of as a thermonuclear word processor.\" — Neal Stephenson" "Cached quotation displayed in the dashboard footer. Seeded with a static fallback; replaced asynchronously at startup and refreshed each time the dashboard is rebuilt.") (defun my/dashboard-fetch-quote () "Fetch a random quote asynchronously and store it in `my/dashboard-quote'." (quotable-random :max-length (window-body-width) :callback (lambda (quotes) (setq my/dashboard-quote (quotable-format-quote (car quotes)))))) :config (define-advice dashboard-random-footer (:override () my/dashboard-random-footer-quote) "Return the most recently fetched quotation." my/dashboard-quote) (my/dashboard-fetch-quote) :hook (dashboard-mode . my/dashboard-fetch-quote))
Fonts
we currently use the Monofur font because we like its rounded shapes, dotted zero 0, and paw bullet •. we do want to edit it one day to be more legible in certain areas (the a can get very illegible at bold weights, for example), but for now, it works for our purposes.
(use-package emacs :config <<set-noto-fonts>> :custom-face (default ((t (:family "monofur")))) (variable-pitch ((t (:family "monofur")))))
Multilingual Coverage with Noto
emacs' glyph fallback feature is a bit inconsistent, so we explicitly set certain scripts to certain fonts. we don't personally need all of them, but we do want some of them. we are also going to use this to add emoji support via Noto Color Sans and Symbola.
(dolist (entry '(;; pkg:github/notofonts/arabic (arabic . "Noto Sans Arabic") ;; pkg:github/notofonts/greek (greek . "Noto Sans") ;; pkg:github/notofonts/noto-cjk (han . ("Noto Sans CJK SC" "Noto Sans CJK TC" "Noto Sans CJK JP")) ;; pkg:github/notofonts/hangul (hangul . "Noto Sans CJK KR") ;; pkg:github/notofonts/hebrew (hebrew . "Noto Sans Hebrew") ;; pkg:github/googlefonts/noto-emoji ;; pkg:github/ChiefMikeK/ttf-symbola (symbol . ("Noto Color Emoji" "Symbola")))) (let ((script (car entry)) (font (cdr entry))) (if (listp font) (progn (set-fontset-font "fontset-default" script (car font)) (dolist (f (cdr font)) (set-fontset-font "fontset-default" script f nil 'append))) (set-fontset-font "fontset-default" script font))))
Nerd Icons
multiple packages later on rely on nerd font icons to display additional glyphs.
to add these icons without needing to use a Nerd Font (monofur has a nerd font, but
has weird rendering issues sometimes in our experience), we can use the package
nerd-icons.el along with using M-x nerd-icons-install-fonts to install the base
symbol font Symbols Nerd Font Mono automatically. we will also be setting the
variable nerd-icons-font-family to said font to make sure that font is used.
;;pkg:github/rainstormstudio/nerd-icons.el (use-package nerd-icons :ensure t :demand t :custom (nerd-icons-font-family "Symbols Nerd Font Mono"))
Transparency
we like being able to see our desktop wallpaper, even while we are coding or writing. thus, we make our terminal and text editors have a transparent background, both on the current frame and every frame after. we also turn on current line highlighting to make it more obvious which line we are editing.
(use-package emacs :config (set-frame-parameter nil 'alpha-background 50) (add-to-list 'default-frame-alist '(alpha-background . 50)) (global-hl-line-mode 1))
Editor Behavior
this section covers the logical behavior of the editor.
*scratch* Buffer
we replace the initial comment in the scratch buffer with a lexical binding cookie so that any code evaluated there benefits from lexical binding.
coopicopy
(use-package emacs :custom (initial-scratch-message ";; -*- lexical-binding: t; -*-\n\n"))
Global Minor Modes
emacs comes with a bunch of extra minor modes that add little quality of life improvements. it should be a no-brainer to include them.
saveplace-modesaves the location of the cursor when visiting a file. loading the file up again will place the cursor back where it was when you closed it.delete-selection-modemakes inserting any text while a selection is active delete the entire selection and replace it with the inserted text… like pretty much every other text editor.repeat-modeallows many multi-key shortcuts to be repeated by just pressing the last key in the sequence.global-so-long-modeif you are editing a file with incredibly long lines (minified JS, machine-generated files, etc.), this mode replaces the typical major mode with an alternative that makes editing those files a lot easier.electric-pair-modeautomatically matches opening delimiter inputs (parentheses, braces, brackets, etc.) with a closing one (e. typing{will automatically insert a}to the right of the cursor to produce{|}).global-subword-modewhen executing motions based on words and tokens, treatcamelCaseandsnake_casetokens as sequences of words instead of one monolithic token (e.camelandCaseare treated as two separate words along withsnakeandcaserespectively).recentf_modemaintains a list of recently visited files for use by things like the dashboard package.coopicopy
(use-package emacs :init (save-place-mode 1) (delete-selection-mode 1) (repeat-mode 1) (global-so-long-mode 1) (electric-pair-mode 1) (global-subword-mode 1) (recentf-mode 1))
Tab Bar
emacs comes with a built-in tab bar! yay! ….but honestly, because we have buffers and other shortcuts to switch between tabs, we don't need a visual tab bar to keep track of our open tabs. so, we enable the tab bar but visually hide it after startup.
note: might change this l8r because this one likes tabs lol
coopicopy
(use-package emacs :init (tab-bar-mode 1) :custom (tab-bar-show nil))
Window History
did you know that you can save a history of window configurations and bring them
back up whenever you want? that's amazing!!!!! let's do that with winner-mode.
problem 1: if you get tabs involved, you can end up restoring layouts into
tabs that never had that layout. whoops. luckily, tab-bar-history-mode solves
this by recording window history for each tab separately. this obviously
begs the question of why tabs and windows are reversed in hiearchy compared to
most environments, but that is a story for another time.
problem 2: the default bindings for switching between window layouts are
kinda silly. who uses the arrow keys in 2026??? hello???? so, we are
changing the bindings to C-x w plus n for next and p for previous like
most other things in emacs. we will also be changing the repeat-map to
reflect the new bindings.
coopicopy
(use-package winner :init (winner-mode 1) (tab-bar-history-mode 1) :custom (winner-dont-bind-my-keys t) :bind (:map window-prefix-map ("n" . winner-undo) ("p" . winner-redo) :map winner-repeat-map ("n" . winner-undo) ("p" . winner-redo)) :config (put #'winner-undo 'repeat-map 'winner-repeat-map) (put #'winner-redo 'repeat-map 'winner-repeat-map))
Smooth Scrolling
we are bringing emacs scrolling to the 21st century!
pixel-scroll-precision-mode makes scrolling operating on a pixel basis rather
than a line basis, which implements part 1 of "smooth scrolling".
pixel-scroll-precision-use-momentum adds inertia to scrolling, meaning
scroll flicks don't abruptly stop but instead glide to a stop, implementing part
2 of "smooth scrolling". to make sure this effect isn't too ridonkulous,
pixel-scroll-precision-momentum-seconds caps the inertia at a maximum length of
0.2 seconds. we want the scrolling to feel intuitive and not like you're
scrolling on ice.
coopicopy
(use-package emacs :init (pixel-scroll-precision-mode 1) :custom (pixel-scroll-precision-use-momentum t) (pixel-scroll-precision-momentum-seconds 0.2))
Cursor Shape
typically, normal mode in meow (a secret tool that will help us later) makes the cursor a square covering a whole character instead of a bar inbetween characters. in editors like Vim, this accurately models the cursor being placed on top of a character and actions being applied to that character. emacs never does this. the manual explicitly states that the cursor "point" is always between characters, never on a character:
Like other positions, point designates a place between two characters (or before the first character, or after the last character), rather than a particular character. Usually terminals display the cursor over the character that immediately follows point; point is actually before the character on which the cursor sits. [insert bib here l8r]
thus, even tho the different cursors help to visually distinguish the different modes while inside the editor… bro. you have the mode icon right there in the bottom left. the block cursor just creates more confusion than it solves. let's kill it for good and keep the bar cursor on at all times.
coopicopy
(use-package emacs :custom (cursor-type 'bar))
Auto-Revert
when a file changes on-disk, usually because a different program overwrote it,
emacs does not automatically fix the buffer that is pointing to that file
by default. you typically have to use M-x revert-buffer to sync the
buffer back up to the actual state of the file on-disk. that's silly.
global-auto-revert-mode makes it so whenever emacs catches that a buffer's file
has been modified outside of emacs (either by filesystem notifications or just
checking periodically itself), it automatically updates the buffer to reflect the
new changes.
we will also extend this behavior to non-file buffers such as Dired and
remote file buffers with global-auto-revert-non-file-buffers and
auto-revert-remote-files respectively.
coopicopy
(use-package autorevert :init (global-auto-revert-mode 1) :custom (global-auto-revert-non-file-buffers t) (auto-revert-remote-files t))
Savehist
savehist-mode saves minibuffer history across sessions, which is required for
autocompletion frameworks like Vertico and Corfu.
coopicopy
(use-package autorevert :init (global-auto-revert-mode 1) :custom (global-auto-revert-non-file-buffers t) (auto-revert-remote-files t))
Revealing Whitespace
linters and formatters often get angwy at whitespace in weird places. to make sure
they don't get angwy, we add visual aids to visualize whitespace with whitespace-mode.
we also add whitespace-cleanup on save to clean up some of the mess.
we can specify which whitespace characters we see with the whitespace-style list.
here's what we turn on:
facehas to be on in order to add special characters to whitespaces.tabsandtab-markhighlight tabs with glyphs to make them distinguishable from spaces.trailinghighlights spaces and tabs at the end of a line.space-before-tabandspace-after-tabdenotes if spaces have been added on either side of a tab that may mess up formatting.newlineandnewline-markadd newline glyphs.missing-newline-at-eofhighlights the last line when the file does not end with a newline, which some formatters get angwy about.
to denote which glyps are used for each kind of whitespace, we use the
whitespace-display-mappings alist: (<character class> <character to replace>
<glyph options>) (each glyph is tried in order until one is able to be displayed).
we try to use Unicode characters first (› for tabs, · for ordinary spaces, ⋮ for non-breaking
spaces, ¬ and ↵ for newlines) and fall back to ASCII (> and $) if we can't
display Unicode for some reason.
to clean up whitespace upon save, we hook the function up to almost all major modes:
prog-mode, conf-mode, and text-mode covers most of what we would need it for.
coopicopy
(use-package whitespace :hook ((prog-mode conf-mode text-mode) (before-save . whitespace-cleanup)) :custom (whitespace-display-mappings '((tab-mark ?\t [10095 ?\t] [62 ?\t]) (space-mark 32 [183] [46]) (space-mark 160 [8942] [95]) (newline-mark ?\n [172 ?\n] [36 ?\n]) (newline-mark ?\r [182] [35]))) (whitespace-style '(face missing-newline-at-eof newline space-after-tab space-before-tab tab-mark tabs trailing)))
Modal Editing: meow
emacs, unlike most of the other text editors popular with neeeeerds, is not a modal editor. you press a key, and the letter on the key goes into the buffer at the point your cursor is. yay.
problem: emacs has. commands. and features. lots of them. some would say too many
but that's a story for another day. to access all of these features and commands,
typically one would hold down a modifier key as they press a different key on
the keyboard. a classic example is clipboard manipulation: in emacs, you use
C-c (CTRL + C) to cut, M-c (Meta (typically the ALT key) + C) to copy,
and C-y (CTRL + Y) to paste. A bit unusual compared to the standard convention
of CTRL + X, CTRL + C, and CTRL + V respectively, but hey, it still works.
… now imagine doing that. for everything you would want to do in an IDE. No GUI. No buttons. No mouse. Modifier keys on top of modifier keys. then you get RSI and you have to tell your doctor that you got your pinkie strained because you were too busy configuring emacs instead of doing actual work like this one is currently doing right now. this is such a common issue that the emacs community has developed an actual term for it: emacs pink(y/ie).
the easiest way to solve this sounds counter-intuitive but makes sense if you think about it: make it so when you first load emacs… you can't insert text.
jackson: wait, why would you make the text editor not able to edit text? that… seams like the wrong thing to do? don't you want to edit text?
millie: yes. but we can fix that by making it so, if you want to, you can edit text later.
jackson: …how does that work?
millie: ……it's hard to find a good way to explain this to a child, but… you know how the air conditioning in houses have a heating mode and a cooling mode? when it's hot outside, you turn the ac to "cool" so it cools down the air in the house, and when it's cold outside, you turn the ac to "heat" so it heats up the air inside the house instead?
jackson: yeah! cold outside, heat the house; hot outside, cold the house!
millie: mhm. the ac can do different things to the air depending on what "mode" it's in.
millie: …what if we took that idea and applied it to a text editor? when you want to write stuff, your brain is all fired up, and you want to heat up your keyboard with aaaaall of your typing. so you turn emacs into "heat" mode to insert text. then, when you're done writing text, you want to "cool down" and think about things like saving your changes, editing the text you just wrote in big strokes, or switching to a different file. that is when you turn emacs into "cool" mode. and when you do that, all of a sudden, all of the keys on your keyboard become command keys.
jackson: ……command keys…… keys that do stuff?
millie: yup! instead of
sinserting ansinto the file, you could make it save the file you just wrote in. you can makeccopy andvpaste so instead of having to hold theCTRLkey down, you can just press one key instead! your pinkie is saved!jackson: yayyyyyy pinkieeeeeeeeeeee
millie\: \*picks up jackson and holds close, slightly giggling\* thank you for playing along with doll. this was silly, but hopefully you had fun.
jackson: yeah! this… soh cray tick thing is…. cute. like…. i don't like driving the car, cause kitty doesnt know how to drive, but with this, kitty don't have to drive. auntie can drive and i am just being cute.
millie: a…a………… /\*uncontrolable sobbing\*/
narrator: while those two are sorting themselves out, we will be right back with our regularly scheduled emacs config… after these brief messages.
d r i n k w a t er
tl;dr: modal editing is the #1 preventative measure against emacs pinkie, and thus we need to add modal editing to emacs.
the typical solution would be to use evil mode, which emulates vi/vim bindings inside of emacs. that works, and it works very well! but you know what's even more evil than vim?
cats.
jackson: mmmrrroooooowwwwwwwwwwwwwwwwwwww >:[
millie: not you
meow mode is a modal editing package for emacs that borrows its keybindings from a different
modal editor: kakoune. why? kakoune includes a lot of really cool utilities for selecting and editing
certain kinds of text, including a multicursor feature that doesn't require holding down CTRL and
clicking everywhere on the screen you want to edit because it's better than that. so much better.
after getting used to how meow works, this one has never wanted to go back to vim bindings. the
only gripe it currently has is not being able to copy text with y by default, but that's a small price to pay.
because meow mode does not actually define any key bindings by default, we manually define the recommended qwerty layout here too. remind this one to document what all the keys do later. …also, we should probably condense this a little bit later too.
for some reason, we have to demand meow in order for the config to load properly.
(use-package meow :ensure t :demand t :custom (meow-cheatsheet-layout meow-cheatsheet-layout-qwerty) :config (meow-motion-define-key '("j" . meow-next) '("k" . meow-prev) '("<escape>" . ignore)) (meow-leader-define-key ;; Use SPC (0-9) for digit arguments. '("1" . meow-digit-argument) '("2" . meow-digit-argument) '("3" . meow-digit-argument) '("4" . meow-digit-argument) '("5" . meow-digit-argument) '("6" . meow-digit-argument) '("7" . meow-digit-argument) '("8" . meow-digit-argument) '("9" . meow-digit-argument) '("0" . meow-digit-argument) '("/" . meow-keypad-describe-key) '("?" . meow-cheatsheet)) (meow-normal-define-key '("0" . meow-expand-0) '("9" . meow-expand-9) '("8" . meow-expand-8) '("7" . meow-expand-7) '("6" . meow-expand-6) '("5" . meow-expand-5) '("4" . meow-expand-4) '("3" . meow-expand-3) '("2" . meow-expand-2) '("1" . meow-expand-1) '("-" . negative-argument) '(";" . meow-reverse) '("," . meow-inner-of-thing) '("." . meow-bounds-of-thing) '("[" . meow-beginning-of-thing) '("]" . meow-end-of-thing) '("a" . meow-append) '("A" . meow-open-below) '("b" . meow-back-word) '("B" . meow-back-symbol) '("c" . meow-change) '("d" . meow-delete) '("D" . meow-backward-delete) '("e" . meow-next-word) '("E" . meow-next-symbol) '("f" . meow-find) '("g" . meow-cancel-selection) '("G" . meow-grab) '("h" . meow-left) '("H" . meow-left-expand) '("i" . meow-insert) '("I" . meow-open-above) '("j" . meow-next) '("J" . meow-next-expand) '("k" . meow-prev) '("K" . meow-prev-expand) '("l" . meow-right) '("L" . meow-right-expand) '("m" . meow-join) '("n" . meow-search) '("o" . meow-block) '("O" . meow-to-block) '("p" . meow-yank) '("q" . meow-quit) '("Q" . meow-goto-line) '("r" . meow-replace) '("R" . meow-swap-grab) '("s" . meow-kill) '("t" . meow-till) '("u" . meow-undo) '("U" . meow-undo-in-selection) '("v" . meow-visit) '("w" . meow-mark-word) '("W" . meow-mark-symbol) '("x" . meow-line) '("X" . meow-goto-line) '("y" . meow-save) '("Y" . meow-sync-grab) '("z" . meow-pop-selection) '("'" . repeat) '("<escape>" . ignore)) (meow-global-mode 1))
Defining Custom Keymaps: bind-map
it is an absolute pain to define custom keymaps for some reason. bind-map makes this somewhat easier by giving you a special macro to quickly define a new keymap.
;; pkg:github/justbur/emacs-bind-map (use-package bind-map :ensure t :demand t)
Help: helpful
emacs has a really extensive set of help and documentation features built-in. the
problem is that they can look a bit plain and leave some things vague. helpful
helps with that (badum tish) by replacing the standard describe-* commands with
variants with source code, related functions, callers, and better formatting.
coopicopy
;; pkg:github/Wilfred/helpful (use-package helpful :ensure t :bind (([remap describe-function] . helpful-callable) ([remap describe-command] . helpful-command) ([remap describe-variable] . helpful-variable) ([remap describe-key] . helpful-key) ([remap describe-symbol] . helpful-symbol)) :preface (defun my/helpful-override-help (fn &rest args) "Call FN with ARGS, routing describe-* calls through Helpful. Temporarily rebinds `describe-function' and `describe-variable' to their Helpful equivalents. This ensures that any `describe-*' calls made within FN—such as those triggered by Org help links—use Helpful rather than the default help system." (cl-letf (((symbol-function 'describe-function) #'helpful-function) ((symbol-function 'describe-variable) #'helpful-variable)) (apply fn args))) :config (advice-add #'org-link--open-help :around #'my/helpful-override-help))
since we are also using apropos, we will also integrate helpful by patching
the buttons in apropos result buffers to bring up helpful instead of the default
help system.
coopicopy
(use-package helpful :after apropos :config (dolist (fun-bt '(apropos-function apropos-macro apropos-command)) (button-type-put fun-bt 'action (lambda (button) (helpful-callable (button-get button 'apropos-symbol))))) (dolist (var-bt '(apropos-variable apropos-user-option)) (button-type-put var-bt 'action (lambda (button) (helpful-variable (button-get button 'apropos-symbol))))))
Language Server Protocol: Elgot
elgot is an LSP client built into emacs, so it's a no-brainer to use
what we already have. we start it up on all programming modes by hooking it up to
prog-mode.
we will also bind some shortcuts for using elgot under the prefix C-c c (it's
the code prefix!!! so c c:::)
C-c c a(elgot-code-actions) opens a quick-action menu for stuff like auto-imports, quick fixes, refactoring options, etc.C-c c o(elgot-code-actions-organize-imports) runs a macro to sort and deduplicate import statements.C-c c r(elgot-rename) lets you rename a particular symbol across an entire project, with the language server helping to find all the reference sites.C-c c f(elgot-format) runs the language server's formatter on the current buffer or the currently active region.
coopicopy
(use-package eglot :hook (prog-mode . eglot-ensure) :bind (:map eglot-mode-map ("C-c c a" . eglot-code-actions) ("C-c c o" . eglot-code-actions-organize-imports) ("C-c c r" . eglot-rename) ("C-c c f" . eglot-format)))
Syntax Checking: Flymake
instead of having to run your code before an error is caught, Flymake is emacs's
built-in syntax checker that regularly checks your code for relevant errors, warnings,
and notes. typically, Flymake integrates with Elgot to communicate with the language
server. so, all we need to do is enable Flymake by hooking it into prog-mode, just
like Elgot.
we will also bind a few keybinds under the prefix C-c ! (oh no! error!):
C-c ! nandC-c ! pnavigate to the next and previous diagnostic in the current buffer respectively.C-c ! lopens a dedicated buffer (*Flymake diagnostics*) specifically for viewing diagnostics across the entire project.
coopicopy
(use-package flymake :hook (prog-mode . flymake-mode) :bind (:map flymake-mode-map ("C-c ! n" . flymake-goto-next-error) ("C-c ! p" . flymake-goto-prev-error) ("C-c ! l" . flymake-show-diagnostics-buffer)))
PCRE Regular Expressions: pcre2el
emacs has regular expressions built in!!! yay!!!! problem: they're not like all the
other girls regular expressions. pcre2el fixes this by allowing you to convert
elisp regex to standard PCRE regex and back again, as well as to rx which this one
has no idea what it does lol.
coopicopy
rxt-global-modeactivates the full key-binding set globally. The bindings all live underC-c /and follow a mnemonic pattern:C-c / p econverts a PCRE regexp to Emacs syntax,C-c / e pdoes the reverse,C-c / /explains the regexp at point inrxform with synchronized highlighting, andC-c / ttoggles between Elisp string andrxform in-place.pcre-query-replace-regexp(C-c / %) is particularly convenient: it lets you write the search pattern in PCRE for an interactivequery-replace, sparing you from translating it mentally.
;; pkg:github/dcolascione/pcre2el (use-package pcre2el :ensure t :init (rxt-global-mode 1))
Completion
completion in emacs is handled in two different and distinct ways: inside a regular buffer, and inside the minibuffer.
in a regular buffer, completion-at-point is used to search for completion results based on the current token. this is what microsoft's
visual studio and visual studio code products call "intellisense": the box of options that appear below your
cursor to show you the completion options.
in the minibuffer, completing-read is used to search for completion results based on what you have input into the minibuffer.
this is how you can get a list of options to select from when you call functions like find-file.
these two systems are technically independent from each other but they do complement each other quite well. there are also utilities like
orderless than can extend them both at the same time.
there are a small set of packages that we are going to use to extend completion:
verticoadds a new vertical (eh? eh?) ui to microbuffer completion that allows you to select from a variety of options.marginaliaadds annotations to each microbuffer completion candidate, giving you more information about each option than just their name can provide.consultadds additional commands for searching and navigating across emacs that usecompleting-readto show the available options.corfuadds an intellisense-style popup tocompletion-at-point, allowing you to select between multiple completion options.capeadds an extension engine tocompletion-at-pointthat allows you to have way more types of inbuffer completion available to you.embarkadds contextual actions to every completion token, allowing you to perform more functions on that token than just "feed this thing into what i'm already doing".orderlessadds partial completion to all completion scenarios, allowing you to match any part of a completion item in any order.
with these seven power stones combined, on top of what we've already cooked with LSP stuff… wow. emacs is basically an IDE now.
;;; completion.el --- Completion stack -*- lexical-binding: t; -*-
Vanilla Minibuffer Tweaks
here's a bunch of vanilla tweaks we can make to improve the minibuffer experience:
C-glets us exit out of a command in progress, but oftentimes if we focus out of the minibuffer, it won't actually do what we mean it to do. so, we're adding akeyboard-quit"advice" (note: what the fuck does this mean) to make sure if we pressC-g, no matter where we're at or what we're currently doing, we get out of emacs jail for free.- embark needs to be able to call
completing-readwhile already inside acompleting-read, soenable-recursive-minibuffersis required. however, to make sure we're able to tell that we're doing minibufferception, we'll also enableminibuffer-depth-indicate-modeto show us how many layers deep we've gone. - by default,
M-xcompletes to every single command available to us. that's silly because many of those commands just won't work in modes that they're not designed for. so,read-extended-command-predicatemakes sure to exclude any commands that explicitly state they can't work in our current buffer due to its current mode.
(note: there's a lot more stuff here but we're tired lol)
coopicopy
(use-package emacs :preface (define-advice keyboard-quit (:around (orig-fun &rest args) my/keyboard-quit-dwim) "Do-What-I-Mean behavior for `keyboard-quit'. If the region is active, deactivate it. If the current buffer is a completions buffer, close it. If a minibuffer is open but not focused, abort the recursive edit. Otherwise, call `keyboard-quit' normally." (cond ((region-active-p) (apply orig-fun args)) ((derived-mode-p 'completion-list-mode) (delete-completion-window)) ((> (minibuffer-depth) 0) (abort-recursive-edit)) (t (apply orig-fun args)))) :custom (resize-mini-windows t) (enable-recursive-minibuffers t) (read-extended-command-predicate #'command-completion-default-include-p) (minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt)) (read-file-name-completion-ignore-case t) (read-buffer-completion-ignore-case t) (completion-ignore-case t) ;; TAB indents first; completion only fires when already indented. (tab-always-indent 'complete) ;; Cape provides richer text-mode completion as a proper ;; replacement. (text-mode-ispell-word-completion nil) ;; Vertico and Corfu render their own UIs; the built-in buffer is a ;; fallback. (completion-auto-help 'visible) ;; Makes "/u/s/e" expand to "/usr/share/emacs". (completion-pcm-leading-wildcard t) (completion-cycle-threshold nil) ;; Only relevant in the rare fallback scenario where Vertico is not ;; active. (completions-eager-update t) ;; Typing /tmp/ while inside ~/projects/ discards the stale prefix. (file-name-shadow-mode t) :hook (minibuffer-setup . cursor-intangible-mode) :init (minibuffer-depth-indicate-mode 1) (context-menu-mode 1))
Orderless
orderless lets you match any part of a completion token with your search and even combine multiple searches together by separating them with a space.
so, for example, if you were looking for completion-at-point, you could type point comp and match
completion-at-point.
casey: Hey, wait a minute, they aren't even in the right order! What gives???
millie: that's because instead of performing the regex comparisons once from left to right, all valid comparisons are made both ways. you don't have to worry about order because there is no order. it's orderless. get it?
casey: …I think it should be required to at least have a gender in order to make dad jokes.
millie: too bad you can't tell doll what to do because you aren't its dad.
casey: ……I………Kinda am? …whatever. go publish this to its site and go to bed, we're tired.
millie: ……………fric
the main problem with doing completion this way is that, well, you're doing a lot
more work in the matching. to reduce input lag, we'll be using a "lite" mode of
orderless called completion-orderless-fast for single-component searches fewer
than four characters. once you go higher than that or insert a space (which, fun
fact, you can do for inbuffer searches by pressing M-SPC), the main orderless
matcher kicks in.
there is also a completion-orderless-with-initialism style that allows you to
search tokens by their initials. so, for example, if there is a symbol named
pen-pineapple-apple-pen, you can match that symbol with the search ppap.
we are only going to enable this in the minibuffer, as we want to keep
inbuffer completion as quick as possible.
we can also use "dispatchers" to define special rules and syntaxes for matching.
for example, the fast dispatcher only matches literal prefixes of terms by default
(so, comp will only match tokens that start with comp). consult comes with its
own special dispatcher that adds two special syntaxes for matching: word$ anchors
the match to the end of the string (e. ing$ will only match strings that end
in ing), and .ext is treated as a file extension matching at the end of the
string as well (so the .el.a files we backed up our old config to would have a
file extension of .a, not .el, so they would not match with a search for .el
files). we will end up adding word$ again later, but the special consult dispatch
also takes into account its internal "tofu" character suffixes that it uses to
disambiguate similar candidates (todo: figure out what the fuck this means).
finally, there are two built-in dispatchers that allow us to add common search
syntax: orderless-kwd-dispatch lets us negate words with !word, and
orderless-affix-dispatch lets us search for literal prefixes and suffixes
with ^word and word$ respectively.
coopicopy
;; pkg:github/oantolin/orderless (use-package orderless :ensure t :demand t :config (defun completion--consult-suffix () "Return a regexp matching the optional Consult tofu suffix at EOL. Any $ anchor pattern must accommodate these suffixes or it will never match." (if (boundp 'consult--tofu-regexp) (concat consult--tofu-regexp "*\\'") "\\'")) (defun completion--fast-dispatch (word index total) "Use literal-prefix matching for single short components." (and (= index 0) (= total 1) (length< word 4) `(orderless-literal-prefix . ,word))) (orderless-define-completion-style completion-orderless-fast "Fast Orderless style: literal-prefix for short single terms, full thereafter." (orderless-style-dispatchers (list #'completion--fast-dispatch)) (orderless-matching-styles '(orderless-literal orderless-regexp))) ;; Initialism is only active in the minibuffer (not Corfu) to avoid ;; slowing down in-buffer auto-completion. (orderless-define-completion-style completion-orderless-with-initialism "Orderless style that prepends initialism matching." (orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp))) (defun completion--consult-dispatch (word _index _total) "Dispatch Orderless styles with Consult suffix and extension support." (cond ;; word$ → anchor to end of string, tofu-aware. ((string-suffix-p "$" word) `(orderless-regexp . ,(concat (substring word 0 -1) (completion--consult-suffix)))) ;; .ext → file-extension match in file/eshell contexts. ((and (or minibuffer-completing-file-name (derived-mode-p 'eshell-mode)) (string-match-p "\\`\\.\\." word)) `(orderless-regexp . ,(concat "\\." (substring word 1) (completion--consult-suffix)))))) :custom (completion-styles '(orderless basic)) (completion-category-defaults nil) (completion-category-overrides '(;; partial-completion handles wildcard expansion and TRAMP ;; host resolution. (file (styles partial-completion)) (command (styles completion-orderless-with-initialism)) (variable (styles completion-orderless-with-initialism)) (symbol (styles completion-orderless-with-initialism)) ;; Orderless post-filters whatever the LSP server returns. (eglot (styles orderless)) (eglot-capf (styles orderless)))) (orderless-component-separator #'orderless-escapable-split-on-space) (orderless-style-dispatchers (list #'completion--consult-dispatch #'orderless-kwd-dispatch #'orderless-affix-dispatch)))
Vertico
the built-in minibuffer UI sucks. it sucks donkey balls. vertico fixes that. it makes the minibuffer completion UI… vertical. wooaawwwwww. based.
now, multiple completion candidates show up in a vertical list as soon as the
minibuffer is called. you can navigate the list like a typical menu and select them
with RET. the completion UI is the only thing vertico changes; everything else is
handled by other packages.
vertico also offers multiple display formats under vertico-multiform-mode.
if a command produces hiearchal or structured output (such as consult-imenu or
consult-outline), they can be shown in an overlay where indentation is legible.
the regular consult-theme un-verticals the UI but in a smarter way, arranging
options in a flat horizontal strip that allows you to see many options without
taking up too much screen real estate.
when using the minibuffer for filesystem navigation, multiple components are
used to make navigation easier. a hook is added to vertico-sort to make
sure file candidates are pre-sorted by their directories first instead of just
their file names. vertico-directory is an extension that adds more fluid
navigation across directories: RET enters a directory, DEL deletes the last
path component, and M-DEL deletes the last path word instead. no more having to
hold down the delete key to go up two folders.
vertico-suspend allows you to save the state of your current minibuffer session
so when you open a second command inside of a minibuffer, after that command is
complete, you get put right back where you left off. vertico-repeat complements this
by allowing you to repeat the most recent minibuffer command, recreating and
replaying the session from scratch.
we are also adding a command called vertico--restrict-to-matches, bound to
S-SPC, that locks the candidate list to only the currently visible matches.
all further input will only be matched against these candidates instead of the
full candidate list.
coopicopy
;; pkg:github/minad/vertico (use-package vertico :ensure t :demand t :custom (vertico-cycle t) (vertico-count 15) (vertico-resize t) (vertico-scroll-margin 3) :init (vertico-mode 1) :config (require 'vertico-sort nil t) (vertico-multiform-mode 1) (setq vertico-multiform-commands '((consult-imenu buffer indexed) (consult-outline buffer) ;; Flat horizontal display shows many theme options without ;; wasting vertical space. (consult-theme flat)) vertico-multiform-categories '((file (vertico-sort-function . vertico-sort-directories-first)) ;; Grep results have very long lines; a buffer with ;; horizontal scrolling is far more legible than a narrow ;; minibuffer column. (consult-grep buffer))) (vertico-mouse-mode 1) (defun vertico--restrict-to-matches () "Restrict Vertico candidates to the currently displayed set. Inserts an invisible read-only space so Orderless treats existing input as a fixed filter and anything typed afterwards as an additional component." (interactive) (let ((inhibit-read-only t)) (goto-char (point-max)) (insert " ") (add-text-properties (minibuffer-prompt-end) (point-max) '(invisible t read-only t cursor-intangible t rear-nonsticky t)))) :bind (:map vertico-map ("M-?" . minibuffer-completion-help) ("M-TAB" . minibuffer-complete) ("S-SPC" . vertico--restrict-to-matches) ("C-SPC" . embark-select) ("M-e" . embark-export) ("M-c" . embark-collect) ("C-z" . vertico-suspend))) (use-package 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-tidy)) (use-package vertico-suspend :after vertico) (use-package vertico-repeat :after vertico :bind ("M-R" . vertico-repeat) :hook (minibuffer-setup . vertico-repeat-save))
Marginalia
marginalia adds additional context to each minibuffer completion candidate in the form of annotations shown to the right of the candidate, in what would otherwise be empty space. for example, the docstrings can be shown next to commands, file sizes and permissions can be shown next to file names, and packages can be described with their versions and summaries.
Marginalia are marks or annotations placed at the margin of the page of a book or in this case helpful colorful annotations placed at the margin of the minibuffer for your completion candidates. Marginalia can only add annotations to the completion candidates. It cannot modify the appearance of the candidates themselves, which are shown unaltered as supplied by the original command.
some candidates may have multiple levels of annotations, which can be cycled
through using marginalia-cycle, bound to M-A. marginalia-offset makes
the margins right-aligned, while marginalia-align-offset makes sure the last
character doesn't get accidentally cut off by the window border.
nerd-icons-completion is an extension to marginalia that prepends nerd-icons
glyphs to each candidate, allowing you to quickly identify different file types,
for example.
coopicopy
;; pkg:github/minad/marginalia (use-package marginalia :ensure t :demand t :after vertico :custom (marginalia-align 'right) (marginalia-align-offset -1) (marginalia-max-relative-mode-width 0.4) :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :init (marginalia-mode 1)) ;; pkg:github/rainstormstudio/nerd-icons-completion (use-package nerd-icons-completion :ensure t :after marginalia :hook (marginalia-mode . nerd-icons-completion-marginalia-setup) :config (nerd-icons-completion-mode 1))
Corfu
corfu adds a little popup for completions inside of a buffer. you know when you are typing out the name of a function in editors like VSCode and a little box pops up with suggestions of functions available to you? yeah! it's like that, but without copilot constantly begging you to please turn it on please play with it please [the rest of this joke has been locked behind millie premium, donate at least five us dollars to our ko-fi linked on the main website to see this joke]
whenever completion-at-point is invoked (read: whenever you type anything lol),
corfu parses all of the available completion-at-point-functions, assembles a list
of candidates, and displays them under the cursor in a small popup.
the main QOL feature of corfu is that it never selects a candidate for you: if
you press RET or SPC without explicitly engaging with the corfu popup, corfu
will not substitute in the top candidate; it will always fall back to whatever
text you have typed unless you explicitly engage with corfu to select a candidate.
corfu-auto allows you to have the corfu popup automatically display after a short
delay (corfu-auto-delay = 0.2s) and a minimum prefix length (corfu-auto-prefix
= 2 characters). if you wanna complete on just one character, you can always invoke
completion manually with C-c p.
since we are using orderless, and orderless allows you to search multiple components
separated by a special separator character (M-SPC), it wouldn't make sense to close
the popup after a word boundary is reached
(corfu-quit-at-boundary), and it would make sense to delay closing after finding
no valid matches until after a separator is inserted so that you can at least try
one more component before it gives up.
corfu-history-mode integrates with Savehist to save candidate history, meaning
that candidates you've previously selected have higher preference in sort order.
corfu-popupinfo-mode adds another popup (bwwaaahhhh) to corfu that shows
additional documentation for the currently highlighted candidate, pulled from
eldoc. it has slightly more delay than the regular popup to prevent "flashing"
since both popups aren't synced up to appear at the same time.
corfu-indexed-mode allows you to select the first nine candidates with
C-u <N> RET, where N (1-9) corresponds to the number prefix added to each candidate.
when using eshell, autocompletion is disabled since usually you only want to envoke
completion manually with TAB. in eshell and comint buffers, pressing RET will also
submit the current line along with selecting a candidate so you don't have to press
RET twice.
if vertico is currently active in the minibuffer, corfu is disabled so you don't have
two completion UIs butting heads with each other. if a minibuffer prompt has its
own completion-at-point functions (like the eval command M-: which could benefit
from elisp completion), then corfu is turned back on.
coopicopy
;; pkg:github/minad/corfu (use-package corfu :ensure t :demand t :custom (corfu-auto t) (corfu-auto-delay 0.2) (corfu-auto-prefix 2) (corfu-cycle t) (corfu-preselect 'prompt) (corfu-min-width 32) (corfu-max-width 120) (corfu-quit-at-boundary nil) (corfu-quit-no-match 'separator) (corfu-preview-current 'insert) (corfu-on-exact-match 'insert) (corfu-scroll-margin 2) :bind (:map corfu-map ;; Insert an Orderless separator without dismissing the popup. ("M-SPC" . corfu-insert-separator) ;; Transfer the session to Vertico for full Embark integration. ("M-m" . corfu--move-to-minibuffer) ("M-g" . corfu-info-location) ("M-h" . corfu-info-documentation) ("C-q" . corfu-quick-insert) ("C-S-q" . corfu-quick-complete)) :config ;; Activate Corfu in the minibuffer only for prompts that have their own ;; completion-at-point-functions (e.g. M-:, M-!). Leave Vertico alone. (setq global-corfu-minibuffer (lambda () (not (or (bound-and-true-p mct--active) (bound-and-true-p vertico--input) (eq (current-local-map) read-passwd-map))))) (defun corfu--setup-orderless-fast () "Use the fast Orderless style buffer-locally for Corfu. Full Orderless is still available after M-SPC." (setq-local completion-styles '(completion-orderless-fast basic) completion-category-overrides nil completion-category-defaults nil)) (defun corfu--move-to-minibuffer () "Transfer the current Corfu session to the Vertico minibuffer. Enables full Embark integration and multi-component Orderless filtering." (interactive) (pcase completion-in-region--data (`(,beg ,end ,table ,pred ,extras) (let ((completion-extra-properties extras) completion-cycle-threshold completion-cycling) (consult-completion-in-region beg end table pred))))) (defun corfu--eshell-setup () "Configure Corfu conservatively for Eshell mode. Auto-completion is disabled; the popup is still available on TAB." (setq-local corfu-auto nil) (corfu-mode 1)) ;; In Eshell/Comint, RET should insert the candidate AND submit the line. (keymap-set corfu-map "RET" `(menu-item "" nil :filter ,(lambda (&optional _) (when (derived-mode-p 'eshell-mode 'comint-mode) #'corfu-send)))) (setq corfu-popupinfo-delay '(0.4 . 0.2)) (add-to-list 'corfu-continue-commands #'corfu--move-to-minibuffer) (global-corfu-mode 1) (corfu-history-mode 1) (corfu-popupinfo-mode 1) (corfu-echo-mode 1) :hook (corfu-mode . corfu--setup-orderless-fast) (eshell-mode . corfu--eshell-setup)) ;; pkg:github/minad/corfu (built-in extension) (use-package corfu-indexed :after corfu :config (corfu-indexed-mode 1)) (use-package corfu-quick :after corfu) ;; pkg:github/LuigiPiucco/nerd-icons-corfu (use-package nerd-icons-corfu :ensure t :after corfu :config (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))
Cape
cape provides additional extensions for completion-at-point and notably allows you
to combine multiple completion-at-point dispatch models into one single list with
cape-capf-super.
cape's additional hooks include cape-file for completing file paths, cap-dabbrev
for completing words from other open buffers, and cape-elisp-block for completing
elisp symbols inside of elisp code blocks in org files.
there are also additional super-CAPFs added for additional flexibility within certain
modes. in emacs-lisp-mode, the typical elisp-completion-at-point backend is
combined with cape-dabbrev when the cursor is immediately after a colon.
in org-mode, cape-tex, cape-sgml, and cape-emoji (separate, not merged)
add completions for TeX math
escapes, HTML entity references, and emoji shortcodes respectively. in buffers
with elgot integration, cape-capf-buster forces re-querying completion on each
keystroke change, since elgot's caching can result in stale candidates polluting the
list. cape-dabbrev is also merged in. in shell and comint buffers, the primary
source for completion is cape-history followed by file paths.
coopicopy
;; pkg:github/minad/cape (use-package cape :ensure t :demand t :after corfu :bind ("C-c o" . cape-prefix-map) :config (defun cape--setup-elisp () "Set up a super-CAPF for `emacs-lisp-mode' buffers. Suppresses :keyword candidates unless the cursor is after a colon." (setq-local completion-at-point-functions (list (cape-capf-super (cape-capf-predicate #'elisp-completion-at-point (lambda (sym) (or (not (keywordp sym)) (eq (char-before (car completion-in-region--data)) ?:)))) #'cape-dabbrev) ;; Kept separate so it fires inside strings/comments without merging overhead. #'cape-file) cape-dabbrev-min-length 3)) (defun cape--setup-org () "Set up additional CAPFs for `org-mode' buffers. Added separately (not merged) so they trigger in-turn, covering \\alpha, α, and :emoji: syntax respectively." (add-hook 'completion-at-point-functions #'cape-tex nil t) (add-hook 'completion-at-point-functions #'cape-sgml nil t) (add-hook 'completion-at-point-functions #'cape-emoji nil t)) (defun cape--setup-eglot () "Configure a cache-busting super-CAPF for Eglot managed buffers. `cape-capf-buster' is applied before merging so dabbrev candidates are not subject to cache busting (they are cheap to compute on every call)." (setq-local completion-at-point-functions (list (cape-capf-super (cape-capf-buster #'eglot-completion-at-point) #'cape-dabbrev) #'cape-file))) (defun cape--setup-shell () "Set up CAPFs for shell and Comint buffers. History first — it is the most contextually relevant source in a shell." (add-hook 'completion-at-point-functions #'cape-history nil t) (add-hook 'completion-at-point-functions #'cape-file nil t)) (setq cape-dabbrev-min-length 3 cape-dabbrev-check-other-buffers t) :hook (completion-at-point-functions . cape-file) (completion-at-point-functions . cape-dabbrev) (completion-at-point-functions . cape-elisp-block) (emacs-lisp-mode . cape--setup-elisp) (org-mode . cape--setup-org) (eglot-managed-mode . cape--setup-eglot) (shell-mode . cape--setup-shell) (comint-mode . cape--setup-shell) (eshell-mode . cape--setup-shell))
Consult
multiple of emacs's built-in commands such as file search, buffer search, and more
could benefit from completing-read but don't currently integrate with it. consult
fixes this by providing alternative commands that integrate with completing-read
and provide additional qol features. for example, consult-buffer (C-x b) doesn't
just search through your open buffers — it also shows recently opened files
and bookmarked files. you can also preview the file with C-,.
consult--orderless-regexp-compiler wires consult's grep sources to orderless so
consult-ripgrep benefits from the orderless engine. consult-async-min-input
prevents a background process from being spawned until two characters have been
typed, since single-character queries usually don't need a lot of heavy lifting.
coopicopy
;; pkg:github/minad/consult (use-package consult :ensure t :demand t :bind (("C-x b" . consult-buffer) ("C-x 4 b" . consult-buffer-other-window) ("C-x 5 b" . consult-buffer-other-frame) ("C-x p b" . consult-project-buffer) ("C-x r b" . consult-bookmark) ;; Replaces built-in yank-pop with a searchable, previewing picker. ("M-y" . consult-yank-pop) ("M-g g" . consult-goto-line) ("M-g M-g" . consult-goto-line) ("M-g o" . consult-outline) ("M-g m" . consult-mark) ("M-g k" . consult-global-mark) ("M-g i" . consult-imenu) ("M-g I" . consult-imenu-multi) ("M-s l" . consult-line) ("M-s L" . consult-line-multi) ("M-s g" . consult-grep) ("M-s G" . consult-git-grep) ("M-s r" . consult-ripgrep) ("M-s f" . consult-find) ("M-s e" . consult-isearch-history) :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 ("M-s" . consult-history) ("M-r" . consult-history)) :hook (completion-list-mode . consult-preview-at-point-mode) :custom (consult-preview-key 'any) ;; 2+ chars before spawning a background process so short inputs ;; don't flood the system. (consult-async-min-input 2) (consult-async-refresh-delay 0.15) (consult-async-input-throttle 0.2) :config ;; Preview only on C-, so a fast buffer switch is not slowed by ;; loading every buffer as the user arrows through the list. (consult-customize consult-buffer :preview-key "C-,") (defun consult--orderless-regexp-compiler (input type &rest _config) "Compile INPUT into Orderless components for Consult async sources." (let ((components (cdr (orderless-compile input)))) (cons (mapcar (lambda (r) (consult--convert-regexp r type)) components) (lambda (str) (orderless--highlight components t str))))) (defun consult--ripgrep-with-orderless (&rest args) "Call ARGS with the Orderless regexp compiler active." (let ((consult--regexp-compiler #'consult--orderless-regexp-compiler)) (apply args))) (advice-add #'consult-ripgrep :around #'consult--ripgrep-with-orderless) (with-eval-after-load 'project (keymap-set project-prefix-map "g" #'consult-ripgrep)))
Embark
embark adds contextual menus to completion candidates that let you decide to do
something else with the candidate instead of, ya know, the thing you were
already doing. you can bring up the context menu with C-. and offers things
like renaming and copying files, finding definitions and calls for functions,
and killing and displaying buffers.
and you don't even have to leave the minibuffer in order to do any of this:
embark-quit-after-action being set to nil means that after an embark action
is performed, you're put right back in your minibuffer exactly where you left off.
embark-mixed-indicator first shows a brief key hint overlay before showing the full
*Embark Actions* buffer after a short pause, in case you already know what you are
doing and don't need to be flashbanged by a buffer change every single time you
want to rename a file goddammit.
embark-bindings (C-h b) lists all active bindings in the current context using
embark's backend, which is an improvement over describe-bindings.
embark--shrink-vertico-for-live automatically squishes vertico's buffer down
to its horizontal single-line mode when embark is called so you can have both
buffers active simultaneously.
embark-consult bridges both packages together: performing embark actions on a
consult grep result buffer can export matches to a special compilation-style buffer
with E (todo: what the fuck does this mean), and live consult preview is actve while
navigating an embark collect buffer (todo: jesse. what).
coopicopy
;; pkg:github/oantolin/embark (use-package embark :ensure t :demand t :bind (("C-." . embark-act) ("M-." . embark-dwim) ("C-h b" . embark-bindings)) :custom (embark-indicators '(embark-mixed-indicator embark-highlight-indicator embark-isearch-highlight-indicator)) (embark-quit-after-action nil) :config (defun embark--shrink-vertico-for-live () "Shrink Vertico when an Embark Live collect buffer appears." (when-let* ((win (and (string-prefix-p "*Embark Live" (buffer-name)) (active-minibuffer-window)))) (with-selected-window win (when (and (bound-and-true-p vertico--input) (fboundp 'vertico-multiform-unobtrusive)) (vertico-multiform-unobtrusive))))) :hook (embark-collect-mode . embark--shrink-vertico-for-live)) (use-package embark-consult :ensure t :demand t :after (embark consult) :hook (embark-collect-mode . consult-preview-at-point-mode))
Dabbrev
\*dabs\*
now that we've gotten that out of the way, we've mentioned dabbrev before but never
really explained what it is: it searches open buffers for words that match the
current prefix and adds them as completion candidates. it's been in emacs since
the stone ages because It Just Works™: if you have multiple buffers open
pointing to, say, multiple files in a project you're working on, it's likely that
you're going to want to reference symbols found in other buffers in your current one.
you don't need to know what language you're in to say Fo is likely going to match
FooBar if all of your other open buffers have FooBar mentioned in them at least
once.
dabbrev is used as a backend for cape, but we can also invoke it manually with M-/.
instead of the default dabbrev-expand command, we are instead binding it to
dabbrev-completion to take advantage of corfu, binding the former command to
C-m-/ instead.
coopicopy
(use-package dabbrev :custom (dabbrev-ignored-buffer-regexps '("\\` ")) :bind (("M-/" . dabbrev-completion) ("C-M-/" . dabbrev-expand)) :config (dolist (mode '(authinfo-mode doc-view-mode pdf-view-mode tags-table-mode)) (add-to-list 'dabbrev-ignored-buffer-modes mode)))
Project & Source Management
since we aim for emacs to be a replacement to traditional ide's like vscode and zed, there's a bunch of stuff that we need to add to make emacs feel more like a graphical ide. we don't necessarily need all of this, but it helps a lot with organizing and working with projects.
terminal / shell stuff will be covered in a different section.
Projectile
projectile is a way to easily keep track of your projects and switch between them.
most of our "projects" lie within a dedicated git/ folder within our Documents.
(use-package projectile :ensure t :bind ("C-c p" . projectile-command-map) :init (setq projectile-project-search-path '("~/Documents/git")) (projectile-mode +1))
Magit
using git in a terminal is so outdated. use git in your editor like a real woman.
it depends on the transient package for its keyboard-based menus.
(use-package transient :ensure t) (use-package magit :ensure t :init (bind-map magit-cmd-map :keys ("C-c G") :bindings ("c" 'magit-commit "a" 'magit-stage-files "p" 'magit-pull "P" 'magit-push "s" 'magit-status "l" 'magit-log "R" 'magit-rebase)))
Treemacs
the one big thing that's missing from emacs, at least for us, is a file explorer tree to be able to process the file state of a project visually while working within the editor. neotree works but is not very mouse-friendly. treemacs aims to solve this and also add some other nicities like displaying org file headings like a table of contents. fun!
treemacs-tag-follow-mode automatically updates itself to follow the current
file or tag.
treemacs-git-mode displays information regarding the current git state of files
and directories. it is set to deferred to highlight both files and directories
after a short delay. treemacs-git-commit-diff-mode also keeps track of how
many commits ahead or behind a project is from its remote.
treemacs-filewatch-mode updates itself after files are
updated. treemacs-indent-guide-mode adds guide lines to indents for better
visual clarity.
there are also additional integration packages with magit and projectile as well as ensuring compatibility with tab mode.
(use-package treemacs :ensure t :init (bind-map treemacs-cmd-map :keys ("C-c t") :bindings ("1" 'treemacs-delete-other-windows "t" 'treemacs "d" 'treemacs-select-directory "B" 'treemacs-bookmark "C-f" 'treemacs-find-file "C-t" 'treemacs-find-tag)) :bind ("M-0" . treemacs-select-window) :config (treemacs-tag-follow-mode t) (treemacs-filewatch-mode t) (treemacs-git-mode 'deferred) (treemacs-git-commit-diff-mode t) (treemacs-indent-guide-mode t)) (use-package treemacs-projectile :after (treemacs projectile) :ensure t) (use-package treemacs-magit :after (treemacs magit) :ensure t) (use-package treemacs-tab-bar ;;treemacs-tab-bar if you use tab-bar-mode :after (treemacs) :ensure t :config (treemacs-set-scope-type 'Tabs))
Org
org is love. org is life.
;;; org.el --- Org Mode configuration -*- lexical-binding: t; -*-
Variable-width Prose
……okay.
Publishing
utilities for ox-publish
;;; publish.el --- Ox Publish configuration -*- lexical-binding: t; -*-
Generating Atom Feeds: ox-atom
(use-package ox-atom :ensure (:host codeberg :repo "coopi/emacs-ox-atom" :branch "main" :protocol https))
Rendering Code Blocks Correctly: htmlize
(use-package htmlize :ensure t)
Custom Org Blocks: org-defblock
there is a package called org-special-block-extras that adds a whole bunch of new org block types for ox-publish output.
it also adds a couple of utitlies for adding your own custom blocks and link url types called org-defblock and org-deflink respectively.
those utilities are sick and would be really useful for extending the functionality of our org blocks, but we don't need all of the extra blocks
that the package comes with — we would much rather build the blocks we actully need ourselves. luckly, someone else had this same issue and
decided to make a fork that includes just the -defblock and -deflink utilities. yippeeeeee!
;; pkg:github/cashpw/org-defblock (use-package org-defblock :ensure (:host github :repo "cashpw/org-defblock" :branch "main" :protocol https))
Publish Script: gen.el
based heavily off of coopi's publish script.
(use-package gen :load-path "/home/mommy/Documents/git/org-site/")