~/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

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-items can 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-mode saves 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-mode makes 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-mode allows many multi-key shortcuts to be repeated by just pressing the last key in the sequence.
  • global-so-long-mode if 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-mode automatically 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-mode when executing motions based on words and tokens, treat camelCase and snake_case tokens as sequences of words instead of one monolithic token (e. camel and Case are treated as two separate words along with snake and case respectively).
  • recentf_mode maintains 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:

  • face has to be on in order to add special characters to whitespaces.
  • tabs and tab-mark highlight tabs with glyphs to make them distinguishable from spaces.
  • trailing highlights spaces and tabs at the end of a line.
  • space-before-tab and space-after-tab denotes if spaces have been added on either side of a tab that may mess up formatting.
  • newline and newline-mark add newline glyphs.
  • missing-newline-at-eof highlights 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 s inserting an s into the file, you could make it save the file you just wrote in. you can make c copy and v paste so instead of having to hold the CTRL key 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 ! n and C-c ! p navigate to the next and previous diagnostic in the current buffer respectively.
  • C-c ! l opens 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-mode activates the full key-binding set globally. The bindings all live under C-c / and follow a mnemonic pattern: C-c / p e converts a PCRE regexp to Emacs syntax, C-c / e p does the reverse, C-c / / explains the regexp at point in rx form with synchronized highlighting, and C-c / t toggles between Elisp string and rx form in-place. pcre-query-replace-regexp (C-c / %) is particularly convenient: it lets you write the search pattern in PCRE for an interactive query-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:

  • vertico adds a new vertical (eh? eh?) ui to microbuffer completion that allows you to select from a variety of options.
  • marginalia adds annotations to each microbuffer completion candidate, giving you more information about each option than just their name can provide.
  • consult adds additional commands for searching and navigating across emacs that use completing-read to show the available options.
  • corfu adds an intellisense-style popup to completion-at-point, allowing you to select between multiple completion options.
  • cape adds an extension engine to completion-at-point that allows you to have way more types of inbuffer completion available to you.
  • embark adds 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".
  • orderless adds 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-g lets 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 a keyboard-quit "advice" (note: what the fuck does this mean) to make sure if we press C-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-read while already inside a completing-read, so enable-recursive-minibuffers is required. however, to make sure we're able to tell that we're doing minibufferception, we'll also enable minibuffer-depth-indicate-mode to show us how many layers deep we've gone.
  • by default, M-x completes 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-predicate makes 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,
&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 &mdash; 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&trade;: 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 &mdash; 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/")