67 minutes
Emacs Configuration
I wish I knew an inspirational quote to put here…
Introduction
Since everybody seems to be writing literate Emacs configs these days, I wanted to give it a spin as well.
I’m not really very proficient on elisp
, so this config is mostly a collection of code I’ve stolen from others.
Since this document literally is my Emacs configuration, at times it may contain code that is a
work in progress, silly comments, TODO
and FIXMEs
and so on.
References
As many others I’ve been using tecosaurs Emacs config as my base configuration. It’s one of the most elaborated literate configs I’ve seen, with a lot of nice little tweaks. My configuration broadly follows his, but it’s slowly morphing into my own :)
So a big thanks to tecosaur
for making my onboarding to Doom Emacs a smooth experience!
Great Emacs configs
Installing Emacs
Emacs can be installed in many ways. I’m usually on a Mac, so I just use Homebrew.
There’s a couple of Emacs packages in Homebrew, but the most popular seems to be emacs-plus and emacs-mac.
I have been using emacs-mac
for some time now, but was previously running emacs-plus
.
Emacs-mac
Installation instructions are in the README file in the repository, but the TLDR; is something like this.
$ brew tap railwaycat/emacsmacport
$ brew install emacs-mac --with-unlimited-select --with-tree-sitter --with-natural-title-bar --with-native-comp --with-xwidgets --with-librsvg --with-imagemagick --with-emacs-big-sur-icon
See the emacs-mac.rb formula file for a list of supported compilation flags.
Emacs-plus
The emacs-plus is another Homebrew formula for installing Emacs on macOS.
$ brew install emacs-plus@28 --with-modern-papirus-icon --with-debug --with-xwidgets --with-native-comp
Fix annoying max open files for Emacs
When using LSP servers with Emacs, it’s easy to hit the “max open files” error. Emacs doesn’t seem to use
operating system ulimit
, but uses pselect
which is limited to FD_SETSIZE file descriptors, usually 1024.
This means that changing ulimit
will not change the value of FD_SETSIZE compiled into Emacs and macOS
libraries. To overcome this limitation we’ve to set the FD_SETSIZE CFLAG when compiling Emacs.
CFLAGS="-DFD_SETSIZE=10000 -DDARWIN_UNLIMITED_SELECT"
See this blog post for a more detailed description.
Getting started
Describe steps required to do in order to get this up and running on a new machine.
- Install nerd-fonts
Doom modeline required nerd-fonts now. run
M-x nerd-icons-install-fonts
.
Configuration
Remember to run doom sync
after modifiyng this file.
;;; config.el -*- lexical-binding: t; -*-
Personal preferences - The random config dumping ground
Me
(setq user-full-name "Rolf Håvard Blindheim"
user-mail-address "rhblind@gmail.com")
Some functionality uses this to identify you, e.g. GPG configuration, email clients, file templates and snippets.
(defconst user-home-directory (expand-file-name (file-name-as-directory "~"))
"A constant that holds the current users home directory.")
Better defaults
General settings that makes life a bit easier.
(setq auto-save-default t ; I don't want to lose work
delete-by-moving-to-trash t ; Delete files to trash
display-time-24hr-format t ; I dont know the difference between AM and PM
evil-want-fine-undo t ; More granular undos in evil insert mode
evil-ex-substitute-global t ; More often than not, I want /s on ex commands
evil-kill-on-visual-paste nil ; Don't add overwritten text in visual mode to the kill ring
fill-column 120 ; We have lot's of screen estate
mouse-wheel-tilt-scroll t ; Scroll horizontally using the mouse
mouse-wheel-flip-direction t ; Scrolling for oldies
pixel-scroll-precision-mode t ; Enable pixel scroll precision mode (requires Emacs 29)
scroll-margin 10 ; Keep a little scroll margin
sh-shell "sh" ; The shell to use when spawning external commands
undo-limit 16000000 ; Increase undo limit to 16Mb
vc-follow-symlinks nil ; Don't follow symlinks, edit them directly
which-key-idle-delay 0.2 ; Makes which-key feels more responsive
window-combination-resize t ; Take new window space from all other windows (not just the current)
x-stretch-cursor t ; Stretch cursor to the glyph width
)
Here are some modes I always want active.
(display-time-mode 1) ; I want to know what time it is
(drag-stuff-global-mode 1) ; Drag text around
(global-company-mode 1) ; Enable autocomplete all over
(global-goto-address-mode 1) ; A minor mode to render urls and like as links
(global-subword-mode 1) ; Iterate through CamelCase words - Not sure how I like this
(smartparens-global-mode 1) ; Always enable smartparens
(ws-butler-global-mode 1) ; Unobtrusive way to trim spaces on end of lines
I like to have the local leader key bound to ,
.
(setq doom-localleader-key ","
doom-localleader-alt-key "M-,")
Hacks
Too many open files
Sometimes I’m getting the dreaded “Too many open files” error in Emacs, which doesn’t seem to take into account
ulimit
, norsysctl kern.maxfiles
/sysctl kern.maxfilesperproc
. If you know a workaround for this, please let me know!Meanwhile, let’s define a function that clears the existing file notification watches from Emacs as a workaround.
(require 'filenotify) (defun file-notify-rm-all-watches (&optional silent) "Remove all existing file notification watches from Emacs. When `silent' is non-nil, don't activate the progress-reporter." (interactive) (if silent (maphash (lambda (key _value) (file-notify-rm-watch key)) file-notify-descriptors) (let ((progress-reporter (make-progress-reporter (format "Clearing all existing file notification watches... ")))) (maphash (lambda (key _value) (file-notify-rm-watch key)) file-notify-descriptors) (progress-reporter-done progress-reporter))))
So, whenever we’re getting the “Too many open files” error in Emacs, just
M-x file-notify-rm-all-watches
, and we should be good to go again. Since this seems to be happening quite frequently, I’ll just put it on a timer for now.;; Run every 5 minutes (run-with-timer 0 (* 5 60) 'file-notify-rm-all-watches t)
GPG, SSH and encryption
For GPG I want to use ~/.authinfo.gpg
since I usually keep this file in my dotfiles repository. It’s easy to
just copy it in place whenever I’m on a new computer.
(after! epa
(setq auth-source-cache-expiry nil ; Don't expire cached auth sources.
epa-file-select-keys nil ; If non-nil, always asks user to select recipients.
epa-file-cache-passphrase-for-symmetric-encryption t ; Cache passphrase for symmetrical encryptions.
epg-gpg-program (cond ((eq system-type 'darwin) "/usr/local/bin/gpg")
((eq system-type 'gnu/linux) "/usr/bin/gpg")
((eq system-type 'windows-nt) "C:/Program Files (x86)/GNU/GnuPG/gpg2")))
(pinentry-start)
;; (load-file (expand-file-name "secrets.el.gpg" doom-private-dir)) ; This cause weird errors
)
I’m using gpg-agent
instead of ssh-agent
so we need to connect here.
(shell-command "gpg-connect-agent updatestartuptty /bye >/dev/null")
Keybindings
Event though Doom Emacs makes a lot of stuff easy, I always needs to look up how to bind keymaps.
Window navigation
Select windows using M-[1..9]
.
(map! "M-0" #'treemacs-select-window ;; make treemacs accessible as Window #0
"M-1" #'winum-select-window-1
"M-2" #'winum-select-window-2
"M-3" #'winum-select-window-3
"M-4" #'winum-select-window-4
"M-5" #'winum-select-window-5
"M-6" #'winum-select-window-6
"M-7" #'winum-select-window-7
"M-8" #'winum-select-window-8
"M-9" #'winum-select-window-9)
Close temporary windows in the current frame.
(defun cust/close-temporary-window ()
"Close all temporary windows in the current frame.
Returns t if any windows were closed."
(interactive)
(save-selected-window
(let (result)
(dolist (window (window-list))
(select-window window)
(cond ((or (eq major-mode #'help-mode)
(eq major-mode #'compilation-mode)
(eq major-mode #'completion-list-mode)
(eq major-mode #'grep-mode)
(eq major-mode #'apropos-mode))
(quit-window)
(setf result t))
((eq major-mode #'magit-popup-mode)
(magit-popup-quit)
(setf result t))))
result)))
;; Bind the function to ESC
(let ((key
(if window-system (kbd "<escape>") "\M-q")))
(global-set-key key #'cust/close-temporary-window))
Workspace navigation
Some of the default Doom workspace navigation keybindings seems a bit counter intuitive for my workflow, so I rebind some of them.
(map! :leader
(:when (modulep! :ui workspaces)
(:prefix-map ("l" . "workspace") ; Rebind workspaces to SCP-l
:desc "Delete this workspace" "d" #'+workspace/delete
"." nil
:desc "Switch to workspace" "l" #'+workspace/switch-to
:desc "Load workspace from file" "L" #'+workspace/load
"]" nil
:desc "Next workspace" "k" #'+workspace/switch-right
"[" nil
:desc "Previous workspace" "j" #'+workspace/switch-left
"`" nil
:desc "Cycle last workspace" "TAB" #'+workspace/other)))
Buffer navigation
I like cycling between the two last buffers using SPC-Tab
.
(map! :leader :desc "Cycle last buffer" "TAB" #'evil-switch-to-windows-last-buffer) ; Use SPC-Tab to cycle between two last buffers
Move around buffers using SPC-b [1..9]
.
The following functions are ported directly from Spacemacs
and relies only at winum
.
(after! winum
(defun move-buffer-to-window (windownum follow-focus-p)
"Moves a buffer to a window, using the window numbering. follow-focus-p controls
whether focus moves to new window (with buffer), or stays on current."
(interactive)
(if (> windownum (length (window-list-1 nil nil t)))
(message "No window numbered %s" windownum)
(let ((b (current-buffer))
(w1 (selected-window))
(w2 (winum-get-window-by-number windownum)))
(unless (eq w1 w2)
(set-window-buffer w2 b)
(switch-to-prev-buffer)
(unrecord-window-buffer w1 b))
(when follow-focus-p
(select-window (winum-get-window-by-number windownum))))))
(defun swap-buffers-to-window (windownum follow-focus-p)
"Swaps visible buffers between active window and selected window. follow-focus-p
controls whether focus moves to new window (with buffer), or stays on current."
(interactive)
(if (> windownum (length (window-list-1 nil nil t)))
(message "No window numbered %s" windownum)
(let* ((b1 (current-buffer))
(w1 (selected-window))
(w2 (winum-get-window-by-number windownum))
(b2 (window-buffer w2)))
(unless (eq w1 w2)
(set-window-buffer w1 b2)
(set-window-buffer w2 b1)
(unrecord-window-buffer w1 b1)
(unrecord-window-buffer w2 b2)))
(when follow-focus-p (winum-select-window-by-number windownum))))
;; define and evaluate three numbered functions
;; - buffer-to-window-1 to 9
;; - move-buffer-to-window-no-follow-1 to 9
;; - swap-buffer-to-window-no-follow-1 to 9
(dotimes (i 9)
(let ((n (+ i 1)))
(eval `(defun ,(intern (format "buffer-to-window-%s" n)) (&optional arg)
,(format "Move buffer to window number %i." n)
(interactive "P")
(if arg
(swap-buffers-to-window ,n t)
(move-buffer-to-window ,n t))))
(eval `(defun ,(intern (format "move-buffer-to-window-no-follow-%s" n)) ()
(interactive)
(move-buffer-to-window ,n nil)))
(eval `(defun ,(intern (format "swap-buffer-window-no-follow-%s" n)) ()
(interactive)
(swap-buffers-to-window ,n nil))))))
Map the newly defined function to SPC-b [1..9]
for easy access.
(map! :leader
(:prefix-map ("b" . "buffer")
:desc "Move buffer to window 1" "1" #'buffer-to-window-1
:desc "Move buffer to window 2" "2" #'buffer-to-window-2
:desc "Move buffer to window 3" "3" #'buffer-to-window-3
:desc "Move buffer to window 4" "4" #'buffer-to-window-4
:desc "Move buffer to window 5" "5" #'buffer-to-window-5
:desc "Move buffer to window 6" "6" #'buffer-to-window-6
:desc "Move buffer to window 7" "7" #'buffer-to-window-7
:desc "Move buffer to window 8" "8" #'buffer-to-window-8
:desc "Move buffer to window 9" "9" #'buffer-to-window-9))
Buffer manipulation
Erase content of current buffer with SPC-b e
(map! :leader
(:prefix-map ("b" . "buffer")
:desc "Erase buffer" "e" #'erase-buffer))
Copy the entire buffer to kill-ring with SPC-b Y
(defun copy-buffer-to-clipboard ()
"Copy entire buffer to clipboard.
This function is from stackoverflow.com (https://stackoverflow.com/a/10216310)"
(interactive)
(clipboard-kill-ring-save (point-min) (point-max)))
(map! :leader
(:prefix-map ("b" . "buffer")
:desc "Copy buffer" "Y" #'copy-buffer-to-clipboard))
Replace buffer with content from clipboard with SPC-b P
.
(defun replace-buffer-from-clipboard ()
"Replace the buffer with content from clipboard."
(interactive)
(delete-region (point-min) (point-max))
(clipboard-yank)
(deactivate-mark))
(map! :leader
(:prefix-map ("b" . "buffer")
:desc "Paste clipboard to buffer" "P" #'replace-buffer-from-clipboard))
Jumping around with Avy
Doom Emacs comes pre-configures with avy
and it is pretty reasonable configured out of the box.
However, I never remember how to use it, so I’ll jot down some notes here. Here’s a quick
YouTube video covering some basic usage.
The basics
g s
- Opens the prefix menu for doing jump navigationg s
- Jump usingevil-avy-goto-char-2
requiring 2 characters before triggeringg s SPC
- Jump in closure usingevil-avy-goto-char-timer
requiring 1 character before triggering
Misc
List all processes running under Emacs.
(map! :leader :desc "List processes" "P" #'list-processes)
Doom configuration
In this section we generate the init.el
file.
;;; init.el -*- lexical-binding: t; -*-
Sneaky garbage collection
Defer garbage collection further back in the startup process.
(setq gc-cons-threshold most-positive-fixnum)
Adopt a sneaky garbage collection strategy of waiting until idle time to collect; staving off the collector while I’m working.
(add-hook 'emacs-startup-hook #'(lambda ()
(setq gcmh-idle-delay 'auto ; or N seconds
gcmh-high-cons-threshold (* 16 1024 1024) ; 16mb
gcmh-verbose nil)))
Load prefer newer
Sometimes it’s necessary to fix a bug, or otherwise change stuff in installed local packages. Always load the newest version of a file.
(setq load-prefer-newer t)
Doom Env
Add some environmental variables to the doom-env-allow
list.
This is required since I like to use gpg-agent
over ssh-agent
for key management, authentication and signing operations.
(when noninteractive
(defvar doom-env-allow '())
(dolist (var '("^LANG$" "^LC_TYPE$" "^GPG_AGENT_INFO$" "^SSH_AGENT_PID$" "^SSH_AUTH_SOCK$"))
(add-to-list 'doom-env-allow var)))
To generate the Emacs environment file, simply run doom env
from the terminal.
Modules
(doom! :completion
<<doom-completion>>
:ui
<<doom-ui>>
:editor
<<doom-editor>>
:emacs
<<doom-emacs>>
:term
<<doom-term>>
:checkers
<<doom-checkers>>
:tools
<<doom-tools>>
:os
<<doom-os>>
:lang
<<doom-lang>>
:email
<<doom-email>>
:app
<<doom-app>>
:config
<<doom-config>>)
Unpinned core packages
In some cases we want to install the latest and greatest version of a package. Doom allows us to unpin packages using the
unpin
macro.(unpin! (:tools lsp tree-sitter))
Interface
(company ; the ultimate code completion backend +childframe) ; ...when your children are better than you ;;helm ; the *other* search engine for love and life ;;ido ; the other *other* search engine... ;;ivy ; a search engine for love and life (vertico ; the search engine of the future +icons)
;;deft ; notational velocity for Emacs doom ; what makes DOOM look the way it does doom-dashboard ; a nifty splash screen for Emacs ;;doom-quit ; DOOM quit-message prompts when you quit Emacs (emoji +unicode) ; 🙂 hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW ;;hydra indent-guides ; highlighted indent columns (ligatures +extra) ; ligatures and symbols to make your code pretty again ;;minimap ; show a map of the code on the side modeline ; snazzy, Atom-inspired modeline, plus API nav-flash ; blink cursor line after big motions ;;neotree ; a project drawer, like NERDTree for vim ophints ; highlight the region an operation acts on (popup +all +defaults) ; tame sudden yet inevitable temporary windows ;;tabs ; a tab bar for Emacs treemacs ; a project drawer, like neotree but cooler ;;unicode ; extended unicode support for various languages vc-gutter ; vcs diff in the fringe vi-tilde-fringe ; fringe tildes to mark beyond EOB (window-select +numbers) ; visually switch windows workspaces ; tab emulation, persistence & separate workspaces zen ; distraction-free coding or writing
(evil +everywhere) ; come to the dark side, we have cookies file-templates ; auto-snippets for empty files fold ; (nigh) universal code folding (format +onsave) ; automated prettiness ;;god ; run Emacs commands without modifier keys ;;lispy ; vim for lisp, for people who don't like vim multiple-cursors ; editing in many places at once ;;objed ; text object editing for the innocent ;;parinfer ; turn lisp into python, sort of ;;rotate-text ; cycle region at point between text candidates snippets ; my elves. They type so I don't have to word-wrap ; soft wrapping with language-aware indent
(dired ; making dired pretty [functional] +icons) electric ; smarter, keyword-based electric-indent (ibuffer +icons) ; interactive buffer management (undo) ; persistent, smarter undo for your inevitable mistakes vc ; version-control and Emacs, sitting in a tree
;;eshell ; the elisp shell that works everywhere ;;shell ; simple shell REPL for Emacs ;;term ; basic terminal emulator for Emacs vterm ; the best terminal emulation in Emacs
syntax ; tasing you for every semicolon you forget ;; (:if ; tasing you for misspelling mispelling ;; (executable-find "aspell") spell +aspell +flyspell) (spell +aspell +flyspell) grammar ; tasing grammar mistake every you make
;;ansible ;;biblio ; Writes a PhD for you (citation needed) (debugger +lsp) ; FIXME stepping through code, to help you add bugs direnv docker editorconfig ; let someone else argue about tabs vs spaces ;;ein ; tame Jupyter notebooks with emacs (eval +overlay) ; run code, run (also, repls) ;;gist ; interacting with github gists (lookup +dictionary +docsets) ; navigate your code and its documentation (lsp +peek) ; M-x vscode (magit +forge) ; a git porcelain for Emacs make ; run make tasks from Emacs ;;pass ; password manager for nerds pdf ; pdf enhancements ;;prodigy ; FIXME managing external services & code builders ;;rgb ; creating color strings ;;taskrunner ; taskrunner for all your projects ;;terraform ; infrastructure as code ;;tmux ; an API for interacting with tmux tree-sitter ; syntax and parsing, sitting in a tree... ;;upload ; map local to remote projects via ssh/ftp
(:if IS-MAC macos) ; improve compatibility with macOS tty ; improve the terminal Emacs experience
Languages
Languages are (usually) not loaded until a file associated with it is opened. Thus we can be pretty liberal with what we enable because there won’t be much overhead.
;;agda ; types of types of types of types... ;;beancount ; mind the GAAP ;;(cc +lsp) ; C > C++ == 1 ;;clojure ; java with a lisp ;;common-lisp ; if you've seen one lisp, you've seen them all ;;coq ; proofs-as-programs ;;crystal ; ruby at the speed of c (csharp +dotnet +tree-sitter +lsp) ; unity, .NET, and mono shenanigans data ; config/data formats ;;(dart +flutter) ; paint ui and not much else ;;dhall (elixir +tree-sitter +lsp) ; erlang done right ;;elm ; care for a cup of TEA? emacs-lisp ; drown in parentheses (erlang +lsp) ; an elegant language for a more civilized age ;;ess ; emacs speaks statistics ;;factor ;;faust ; dsp, but you get to keep your soul ;;fortran ; in FORTRAN, GOD is REAL (unless declared INTEGER) ;;fsharp ; ML stands for Microsoft's Language ;;fstar ; (dependent) types and (monadic) effects and Z3 ;;gdscript ; the language you waited for (go +tree-sitter +lsp) ; the hipster dialect ;;(haskell +lsp) ; a language that's lazier than I am ;;hy ; readability of scheme w/ speed of python ;;idris ; a language you can depend on json ; At least it ain't XML ;;(java +lsp) ; the poster child for carpal tunnel syndrome (javascript +tree-sitter +lsp) ; all(hope(abandon(ye(who(enter(here)))))) ;;julia ; a better, faster MATLAB ;;kotlin ; a better, slicker Java(Script) ;;latex ; writing papers in Emacs has never been so fun ;;lean ; for folks with too much to prove ;;ledger ; be audit you can be lua ; one-based indices? one-based indices markdown ; writing docs for people to ignore ;;nim ; python + lisp at the speed of c ;;nix ; I hereby declare "nix geht mehr!" ;;ocaml ; an objective camel (org ;organize your plain life in plain text +pretty ; yessss my pretties! (nice unicode symbols) +dragndrop ; drag & drop files/images into org buffers +hugo ; use Emacs for hugo blogging +noter ; enhanced PDF notetaking +jupyter ; ipython/jupyter support for babel +pandoc ; export-with-pandoc support +gnuplot ; who doesn't like pretty pictures ;;+pomodoro ; be fruitful with the tomato technique +present ; using org-mode for presentations +roam2) ; wander around notes ;;php ; perl's insecure younger brother ;;plantuml ; diagrams for confusing people more ;;purescript ; javascript, but functional (python ; beautiful is better than ugly +lsp +tree-sitter +cython +poetry +pyright) ;;qt ; the 'cutest' gui framework ever ;;racket ; a DSL for DSLs ;;raku ; the artist formerly known as perl6 ;;rest ; Emacs as a REST client ;;rst ; ReST in peace ;;(ruby +rails) ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"} (rust +lsp) ; Fe2O3.unwrap().unwrap().unwrap().unwrap() ;;scala ; java, but good ;;(scheme +guile) ; a fully conniving family of lisps (sh +powershell +tree-sitter +lsp) ; she sells {ba,z,fi}sh shells on the C xor ;;sml ;;solidity ; do you need a blockchain? No. ;;swift ; who asked for emoji variables? ;;terra ; Earth and Moon in alignment for performance. (web +tree-sitter +lsp) ; the tubes (yaml ; JSON, but readable +lsp) ;;zig ; C, but simpler
Everything in Emacs
;; (mu4e +org +gmail) ;; notmuch ;;(wanderlust +gmail)
;;calendar ;;emms ;;everywhere ; *leave* Emacs!? You must be joking ;;irc ; how neckbeards socialize ;;(rss +org) ; emacs as an RSS reader ;;twitter ; twitter client https://twitter.com/vnought
Visual settings
Font Faces
Cursor seems to not always load the correct color. Explicitly set it to whatever themes “blue” color is.
(custom-set-faces! `(cursor :background ,(doom-color 'blue)))
TODO: The “history” items in mini-buffers and eval-buffer is too pale. Needs better contrast.
Replace the “modified” buffer color in the modeline, so it doesn’t look like something’s wrong every time we edit files.
(custom-set-faces! `(doom-modeline-buffer-modified :foreground "Orange" :italic t))
Frame
Configure default size for new Emacs frames. I’m usually at 1440p display size, so this seems reasonable.
(add-to-list 'default-frame-alist '(height . 72))
(add-to-list 'default-frame-alist '(width . 240))
To make Emacs look a bit more modern (at least on macOS) we can enable a “natural title bar”, which makes
the color of the title bar match the color of the buffer.
For this to work, Emacs must be compiled using the --with-natural-title-bar
flag.
Configuring transparent titlebar is supposed to be working by the following code, however I’m unable to make it work by only configuring Emacs.
(when (eq system-type 'darwin)
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(ns-appearance . light)) ;; or dark
(setq ns-use-proxy-icon nil
frame-title-format nil))
So by setting some defaults
in macOS, we achieve the desired result.
$ defaults write org.gnu.Emacs HideDocumentIcon YES
$ defaults write org.gnu.Emacs TransparentTitleBar LIGHT # or DARK; doesn't really matter when used with "frame-title-format nil"
Theme and modeline
I currently use two themes - a light theme for usual work, and a dark theme for late night hacking sessions.
These days I’m using the doom-tomorrow-day
light theme, and doom-nord-aurora
dark theme. To easily cycle
between them, I keep my favorite themes in a doom-cycle-themes
list, and have a small function that just applies
the next theme in the list.
(setq doom-cycle-themes '(doom-tomorrow-day
doom-nord-aurora))
We’ll use the first theme in the list as our default theme.
(defun cust/get-theme-name (theme)
"Returns the name of `theme'.
If `theme' is a list, return the first item in list."
(if (listp theme) (car theme) theme))
(setq doom-theme (cust/get-theme-name doom-cycle-themes))
Add some functions to easily cycle through doom-cycle-themes
list.
(defun cust/cycle-theme (&optional backward)
"Cycle through themes defined in `doom-cycle-themes'.
When `backward' is non-nil, cycle the list backwards."
(interactive "P")
(let* ((theme-names (mapcar #'cust/get-theme-name doom-cycle-themes))
(themes (if backward (reverse theme-names) theme-names))
(next-theme (car (or (cdr (memq doom-theme themes))
;; if current theme isn't in cycleable themes,
;; start over
themes))))
(when doom-theme
(disable-theme doom-theme))
(let ((progress-reporter
(make-progress-reporter
(format "Loading theme %s... " next-theme))))
(load-theme next-theme t nil)
(progress-reporter-done progress-reporter))))
(defun cust/cycle-theme-backward ()
"Cycle through themes defined in `doom-cycle-themes' backwards."
(interactive)
(cust/cycle-theme t))
And finally add some keybindings to easily toggle them.
(map! :leader
(:prefix-map ("t" . "toggle")
:desc "Next theme" "t" #'cust/cycle-theme
:desc "Previous theme" "T" #'cust/cycle-theme-backward))
Slightly prettier default buffer names.
(setq +doom-dashboard-name "► Doom"
doom-fallback-buffer-name "► Doom")
Add some extra bells and whistles to the Doom modeline.
(setq doom-modeline-icon (display-graphic-p)
doom-modeline-env-version nil ;; too noisy
doom-modeline-major-mode-icon t
doom-modeline-major-mode-color-icon t
doom-modeline-buffer-state-icon t)
Also, in Emacs 29.1 we got support for transparent backgrounds. This can be enabled by the following command if desired.
(set-frame-parameter nil 'alpha-background 70)
Fonts
I’ll just keep the default config description around for now.
;; Doom exposes five (optional) variables for controlling fonts in Doom:
;;
;; - `doom-font' -- the primary font to use
;; - `doom-variable-pitch-font' -- a non-monospace font (where applicable)
;; - `doom-big-font' -- used for `doom-big-font-mode'; use this for
;; presentations or streaming.
;; - `doom-unicode-font' -- for unicode glyphs
;; - `doom-serif-font' -- for the `fixed-pitch-serif' face
;;
;; See 'C-h v doom-font' for documentation and more examples of what they
;; accept. For example:
;;
;;(setq doom-font (font-spec :family "Fira Code" :size 12 :weight 'semi-light)
;; doom-variable-pitch-font (font-spec :family "Fira Sans" :size 13))
;;
;; If you or Emacs can't find your font, use 'M-x describe-font' to look them
;; up, `M-x eval-region' to execute elisp code, and 'M-x doom/reload-font' to
;; refresh your font settings. If Emacs still can't find your font, it likely
;; wasn't installed correctly. Font issues are rarely Doom issues!
(setq doom-font (font-spec :family "Fira Code" :size 13 :weight 'semi-light)
doom-big-font (font-spec :family "Fira Code" :size 18)
doom-variable-pitch-font (font-spec :family "Overpass" :size 18)
doom-unicode-font (font-spec :family "JuliaMono")
doom-serif-font (font-spec :family "IBM Plex Mono" :weight 'light))
“In addition to these fonts, Merriweather is used with nov.el, and Alegreya as a serifed proportional font used by mixed-pitch-mode for writeroom-mode with Org files. Because we care about how things look let’s add a check to make sure we’re told if the system doesn’t have any of those fonts.”
(defvar required-fonts '("JetBrainsMono.*" "Overpass" "JuliaMono" "IBM Plex Mono" "Merriweather" "Alegreya" "Iosevka Aile"))
(defvar available-fonts
(delete-dups (or (font-family-list)
(split-string (shell-command-to-string "fc-list : family")
"[,\n]"))))
(defvar missing-fonts
(delq nil (mapcar
(lambda (font)
(unless (delq nil (mapcar (lambda (f)
(string-match-p (format "^%s$" font) f))
available-fonts))
font))
required-fonts)))
(if missing-fonts
(pp-to-string
`(unless noninteractive
(add-hook! 'doom-init-ui-hook
(run-at-time nil nil
(lambda ()
(message "%s missing the following fonts: %s"
(propertize "Warning!" 'face '(bold warning))
(mapconcat (lambda (font)
(propertize font 'face 'font-lock-variable-name-face))
',missing-fonts
", "))
(sleep-for 0.5))))))
";; No missing fonts detected!")
<<detect-missing-fonts()>>
Line numbers
Set default line number mode to 'relative
, and disable it for certain modes.
(setq display-line-numbers-type 'relative)
Mixed pitch
From the
:ui zen
module.
We’d like to use mixed pitch in certain modes. If we simply add a hook, when directly opening a file with (a new) Emacs mixed-pitch-mode runs
before UI initialisation, which is problematic. To resolve this, we create a hook that runs after UI initialisation and both
- conditionally enables
mixed-pitch-mode
- sets up the mixed pitch hooks
(defvar mixed-pitch-modes '(org-mode LaTeX-mode markdown-mode gfm-mode Info-mode)
"Modes that `mixed-pitch-mode' should be enabled in, but only after UI initialisation.")
(defun init-mixed-pitch-h ()
"Hook `mixed-pitch-mode' into each mode in `mixed-pitch-modes'.
Also immediately enables `mixed-pitch-modes' if currently in one of the modes."
(when (memq major-mode mixed-pitch-modes)
(mixed-pitch-mode 1))
(dolist (hook mixed-pitch-modes)
(add-hook (intern (concat (symbol-name hook) "-hook")) #'mixed-pitch-mode)))
(add-hook 'doom-init-ui-hook #'init-mixed-pitch-h)
As mixed pitch uses the variable mixed-pitch-face
, we can create a new function to apply mixed pitch with a serif face instead of the default.
This was created for writeroom mode.
(autoload #'mixed-pitch-serif-mode "mixed-pitch"
"Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch." t)
(after! mixed-pitch
(defface variable-pitch-serif
'((t (:family "serif")))
"A variable-pitch face with serifs."
:group 'basic-faces)
(setq mixed-pitch-set-height t)
(setq variable-pitch-serif-font (font-spec :family "Alegreya" :size 18))
(set-face-attribute 'variable-pitch-serif nil :font variable-pitch-serif-font)
(defun mixed-pitch-serif-mode (&optional arg)
"Change the default face of the current buffer to a serifed variable pitch, while keeping some faces fixed pitch."
(interactive)
(let ((mixed-pitch-face 'variable-pitch-serif))
(mixed-pitch-mode (or arg 'toggle)))))
Now, as Harfbuzz is currently used in Emacs, we’ll be missing out on the following Alegreya ligatures:
ff ff ffi ffi ffj ffj ffl ffl fft fft fi fi fj fj ft ft Th Th
Thankfully, it isn’t to hard to add these to the composition-function-table
.
;; (set-char-table-range composition-function-table ?f '(["\\(?:ff?[fijlt]\\)" 0 font-shape-gstring]))
;; (set-char-table-range composition-function-table ?T '(["\\(?:Th\\)" 0 font-shape-gstring]))
Transparency
Emacs 29 got support for background transparency which looks pretty nice.
Set up a toggle-transparency
function and map it to M-x t B
.
(set-frame-parameter nil 'alpha-background 100) ;; Current frame
(add-to-list 'default-frame-alist '(alpha-background . 100)) ;; All new frames from now on
(defun cust/toggle-window-transparency ()
"Toggle window transparency"
(interactive)
(let ((alpha (frame-parameter nil 'alpha)))
(set-frame-parameter
nil 'alpha
(if (eql (cond ((numberp alpha) alpha)
((numberp (cdr alpha)) (cdr alpha))
;; Also handle undocumented (<active> <inactive>) form.
((numberp (cadr alpha)) (cadr alpha)))
100)
90 100))))
(map! :leader
(:prefix-map ("t" . "toggle")
:desc "Background transparency" "B" #'cust/toggle-window-transparency))
Windows
I find it rather handy to be asked which buffer I want to see after splitting the window.
First, split the window…
(setq evil-vsplit-window-right t
evil-split-window-below t)
…then, pull up a buffer prompt.
(defadvice! prompt-for-buffer (&rest _)
:after '(evil-window-split evil-window-vsplit)
(consult-buffer))
Buffers
Text scaling
The
default-text-scale
package works by adjusting the size of thedefault
face, so that it affects all buffers at once.(package! default-text-scale)
Enable
default-text-scale-mode
global minor mode and add some extra keybindings for macOS. I think it’s easier to just useCMD-+/-/0
to increase, decrease and reset.(default-text-scale-mode 1) (map! :map default-text-scale-mode-map (:when (eq system-type 'darwin) "s-+" #'default-text-scale-increase "s--" #'default-text-scale-decrease "s-0" #'default-text-scale-reset) (:when (and (modulep! :ui workspaces) (eq system-type 'darwin) (define-key evil-normal-state-map (kbd "s-0") nil))))
Buffer manipulation
These little guys are helpful to remove duplicates in a region or buffer.
(defun uniquify-region-lines (beg end) "Remove duplicate adjacent lines in region." (interactive "*r") (save-excursion (goto-char beg) (while (re-search-forward "^\\(.*\n\\)\\1+" end t) (replace-match "\\1")))) (defun uniquify-buffer-lines () "Remove duplicate adjacent lines in the current buffer." (interactive) (uniquify-region-lines (point-min) (point-max)))
Other things
Splash screen
This beauty is also shamelessly ripped off from tecosaur’s Emacs config!
Make it theme-appropriate, and resize with the frame.
(defvar fancy-splash-image-template
(expand-file-name "misc/splash-images/emacs-e-template.svg" doom-private-dir)
"Default template svg used for the splash image, with substitutions from ")
(defvar fancy-splash-sizes
`((:height 300 :min-height 50 :padding (0 . 2))
(:height 250 :min-height 42 :padding (2 . 4))
(:height 200 :min-height 35 :padding (3 . 3))
(:height 150 :min-height 28 :padding (3 . 3))
(:height 100 :min-height 20 :padding (2 . 2))
(:height 75 :min-height 15 :padding (2 . 1))
(:height 50 :min-height 10 :padding (1 . 0))
(:height 1 :min-height 0 :padding (0 . 0)))
"list of plists with the following properties
:height the height of the image
:min-height minimum `frame-height' for image
:padding `+doom-dashboard-banner-padding' (top . bottom) to apply
:template non-default template file
:file file to use instead of template")
(defvar fancy-splash-template-colours
'(("$colour1" . keywords) ("$colour2" . type) ("$colour3" . base5) ("$colour4" . base8))
"list of colour-replacement alists of the form (\"$placeholder\" . 'theme-colour) which applied the template")
(unless (file-exists-p (expand-file-name "theme-splashes" doom-cache-dir))
(make-directory (expand-file-name "theme-splashes" doom-cache-dir) t))
(defun fancy-splash-filename (theme-name height)
(expand-file-name (concat (file-name-as-directory "theme-splashes")
theme-name
"-" (number-to-string height) ".svg")
doom-cache-dir))
(defun fancy-splash-clear-cache ()
"Delete all cached fancy splash images"
(interactive)
(delete-directory (expand-file-name "theme-splashes" doom-cache-dir) t)
(message "Cache cleared!"))
(defun fancy-splash-generate-image (template height)
"Read TEMPLATE and create an image if HEIGHT with colour substitutions as
described by `fancy-splash-template-colours' for the current theme"
(with-temp-buffer
(insert-file-contents template)
(re-search-forward "$height" nil t)
(replace-match (number-to-string height) nil nil)
(dolist (substitution fancy-splash-template-colours)
(goto-char (point-min))
(while (re-search-forward (car substitution) nil t)
(replace-match (doom-color (cdr substitution)) nil nil)))
(write-region nil nil
(fancy-splash-filename (symbol-name doom-theme) height) nil nil)))
(defun fancy-splash-generate-images ()
"Perform `fancy-splash-generate-image' in bulk"
(dolist (size fancy-splash-sizes)
(unless (plist-get size :file)
(fancy-splash-generate-image (or (plist-get size :template)
fancy-splash-image-template)
(plist-get size :height)))))
(defun ensure-theme-splash-images-exist (&optional height)
(unless (file-exists-p (fancy-splash-filename
(symbol-name doom-theme)
(or height
(plist-get (car fancy-splash-sizes) :height))))
(fancy-splash-generate-images)))
(defun get-appropriate-splash ()
(let ((height (frame-height)))
(cl-some (lambda (size) (when (>= height (plist-get size :min-height)) size))
fancy-splash-sizes)))
(setq fancy-splash-last-size nil)
(setq fancy-splash-last-theme nil)
(defun set-appropriate-splash (&rest _)
(let ((appropriate-image (get-appropriate-splash)))
(unless (and (equal appropriate-image fancy-splash-last-size)
(equal doom-theme fancy-splash-last-theme)))
(unless (plist-get appropriate-image :file)
(ensure-theme-splash-images-exist (plist-get appropriate-image :height)))
(setq fancy-splash-image
(or (plist-get appropriate-image :file)
(fancy-splash-filename (symbol-name doom-theme) (plist-get appropriate-image :height))))
(setq +doom-dashboard-banner-padding (plist-get appropriate-image :padding))
(setq fancy-splash-last-size appropriate-image)
(setq fancy-splash-last-theme doom-theme)
(+doom-dashboard-reload)))
(add-hook 'window-size-change-functions #'set-appropriate-splash)
(add-hook 'doom-load-theme-hook #'set-appropriate-splash)
Now the only thing missing is a an extra interesting line, whether that be some corporate BS, an developer excuse, or a fun (useless) fact.
The following is rather long, but it essentially
- fetches a phrase from an API
- inserts it into the dashboard (asynchronously)
- moves
point
to the phrase - re-uses the last phrase for requests within a few seconds of it being fetched
(defvar splash-phrase-source-folder
(expand-file-name "misc/splash-phrases" doom-private-dir)
"A folder of text files with a fun phrase on each line.")
(defvar splash-phrase-sources
(let* ((files (directory-files splash-phrase-source-folder nil "\\.txt\\'"))
(sets (delete-dups (mapcar
(lambda (file)
(replace-regexp-in-string "\\(?:-[0-9]+-\\w+\\)?\\.txt" "" file))
files))))
(mapcar (lambda (sset)
(cons sset
(delq nil (mapcar
(lambda (file)
(when (string-match-p (regexp-quote sset) file)
file))
files))))
sets))
"A list of cons giving the phrase set name, and a list of files which contain phrase components.")
(defvar splash-phrase-set
(nth (random (length splash-phrase-sources)) (mapcar #'car splash-phrase-sources))
"The default phrase set. See `splash-phrase-sources'.")
(defun splase-phrase-set-random-set ()
"Set a new random splash phrase set."
(interactive)
(setq splash-phrase-set
(nth (random (1- (length splash-phrase-sources)))
(cl-set-difference (mapcar #'car splash-phrase-sources) (list splash-phrase-set))))
(+doom-dashboard-reload t))
(defvar splase-phrase--cache nil)
(defun splash-phrase-get-from-file (file)
"Fetch a random line from FILE."
(let ((lines (or (cdr (assoc file splase-phrase--cache))
(cdar (push (cons file
(with-temp-buffer
(insert-file-contents (expand-file-name file splash-phrase-source-folder))
(split-string (string-trim (buffer-string)) "\n")))
splase-phrase--cache)))))
(nth (random (length lines)) lines)))
(defun splash-phrase (&optional set)
"Construct a splash phrase from SET. See `splash-phrase-sources'."
(mapconcat
#'splash-phrase-get-from-file
(cdr (assoc (or set splash-phrase-set) splash-phrase-sources))
" "))
(defun doom-dashboard-phrase ()
"Get a splash phrase, flow it over multiple lines as needed, and make fontify it."
(mapconcat
(lambda (line)
(+doom-dashboard--center
+doom-dashboard--width
(with-temp-buffer
(insert-text-button
line
'action
(lambda (_) (+doom-dashboard-reload t))
'face 'doom-dashboard-menu-title
'mouse-face 'doom-dashboard-menu-title
'help-echo "Random phrase"
'follow-link t)
(buffer-string))))
(split-string
(with-temp-buffer
(insert (splash-phrase))
(setq fill-column (min 70 (/ (* 2 (window-width)) 3)))
(fill-region (point-min) (point-max))
(buffer-string))
"\n")
"\n"))
(defadvice! doom-dashboard-widget-loaded-with-phrase ()
:override #'doom-dashboard-widget-loaded
(setq line-spacing 0.2)
(insert
"\n\n"
(propertize
(+doom-dashboard--center
+doom-dashboard--width
(doom-display-benchmark-h 'return))
'face 'doom-dashboard-loaded)
"\n"
(doom-dashboard-phrase)
"\n"))
Lastly, the doom dashboard “useful commands” are no longer useful to me.
So, we’ll disable them and then for a particularly clean look disable
the modeline and hl-line-mode
, then also hide the cursor.
(remove-hook '+doom-dashboard-functions #'doom-dashboard-widget-shortmenu)
(add-hook! '+doom-dashboard-mode-hook (hide-mode-line-mode 1) (hl-line-mode -1))
(setq-hook! '+doom-dashboard-mode-hook evil-normal-state-cursor (list nil))
At the end, we have a minimal but rather nice splash screen.
I haven’t forgotten about the ASCII banner though! Once again we’re going for something simple.
(defun doom-dashboard-draw-ascii-emacs-banner-fn ()
(let* ((banner
'(",---.,-.-.,---.,---.,---."
"|---'| | |,---|| `---."
"`---'` ' '`---^`---'`---'"))
(longest-line (apply #'max (mapcar #'length banner))))
(put-text-property
(point)
(dolist (line banner (point))
(insert (+doom-dashboard--center
+doom-dashboard--width
(concat
line (make-string (max 0 (- longest-line (length line)))
32)))
"\n"))
'face 'doom-dashboard-banner)))
(unless (display-graphic-p) ; for some reason this messes up the graphical splash screen atm
(setq +doom-dashboard-ascii-banner-fn #'doom-dashboard-draw-ascii-emacs-banner-fn))
Utility functions
Here are just a collection of utility functions I use.
align-whitespace
This little gem can align a block of text by whitespace columns, which makes it easy to align text so it looks a bit prettier.
(defun align-whitespace (start end) "Align columns by whitespace" (interactive "r") (align-regexp start end "\\(\\s-*\\)\\s-" 1 0 t))
sort-words
alphabetically handy It’s sometimes sort to words.
(defun sort-words (reverse start end) "Sort words in region alphabetically, in REVERSE if negative. Prefixed with negative \\[universal-argument], sorts in reverse. The variable `sort-fold-case' determines whether alphabetic case affects the sort order. See `sort-regexp-fields'. https://www.emacswiki.org/emacs/SortWords" (interactive "*P\nr") (sort-regexp-fields reverse "\\w+" "\\&" start end))
sort-symbols
(as be deserved sorted symbols to well)
(defun sort-symbols (reverse start end) "Sort symbols in region alphabetically, in REVERSE if negative. See `sort-words'." (interactive "*P\nr") (sort-regexp-fields reverse "\\(\\sw\\|\\s_\\)+" "\\&" start end)) (defun delete-trailing-crlf () "Remove trailing crlf (^M) end-of-line in the current buffer" (interactive) (save-match-data (save-excursion (let ((remove-count 0)) (goto-char (point-min)) (while (re-search-forward (concat (char-to-string 13) "$") (point-max) t) (setq remove-count (+ remove-count 1)) (replace-match "" nil nil)) (message (format "%d ^M removed from buffer." remove-count))))))
add-list-to-list
Sometimes it’s handy to merge a list into another.
(defun add-list-to-list (list-var elements &optional append compare-fn) "Add ELEMENTS to the value of LIST-VAR in order Behaves like `add-to-list', but accepts a list of new ELEMENTS to add." (interactive) (setq elements (if append elements (reverse elements))) (let* ((val (symbol-value list-var)) (lst (if append (reverse val) val))) (dolist (elt elements) (cl-pushnew elt lst :test compare-fn)) (set list-var (if append (nreverse lst) lst))) (symbol-value list-var))
delete-carrage-returns
Some files (looking at you,
dotnet
generated projects) create files using DOS-style end of line characters (CRLF
). I always want to use unix-style end of line characters (LF
).(defun delete-carrage-returns () "Deletes all carrage-returns characters in buffer" (interactive) (save-excursion (goto-char 0) (while (search-forward "\r" nil :noerror) (replace-match ""))))
Packages
This is where to install packages.
By declaring packages using the package!
macro in packages.el
, then running doom refresh
from the command line
(or M-x doom/refresh
).
The packages.el
file should not be byte compiled!
;; -*- no-byte-compile: t; -*-
Loading instructions
Packages in MELPA/ELPA/emacsmirror
To install packages from the default locations:
(package! some-package-name)
Packages from Git repositories
To install packages from git repositories, we need to specify a :recipe
.
Check out the documentation here.
(package! some-package-name
:recipe (:host github :repo "username/repo"))
Unless the repository have a PACKAGENAME.el
file, or it is located in a subdirectory in the repository, we must specify
the location using :file
.
(package! some-package-name
:recipe (:host github :repo "username/repo"
:files ("some-file.el" "src/lisp/*.el")))
It is also possible to install packages from any git repository. If we for example want to install a package from a gist, we could do it by specifying the receipe like this.
(package! some-package-name
:recipe (:host nil
:type git
:repo "https://gist.github.com/61b34f9ca674495eac7f1fe990ebe966.git"
:files ("some-package-name.el")))
Disable or override built-in packages
In order to disable a built-in package (for whatever reason), we can use the :disable
property.
(package! built-in-package-name :disable t)
We can also override built-in packages without having to specify all :recipe
properties. These will
inherit the rest from Doom or MELPA/ELPA/emacsmirror.
(package! built-in-package-name :recipe (:nonrecursive t))
(package! built-in-package-name :recipe (:repo "some-fork/package-name"))
(package! built-in-package-name :recipe (:branch "develop"))
Tools
Auto highlight symbol
Minor mode for automatically highlighting current symbol
(package! auto-highlight-symbol)
(setq ahs-idle-interval 0.2)
(add-hook! 'prog-mode-hook #'auto-highlight-symbol-mode)
The default AHS font faces don’t match the theme at all, let’s try to fix that!
(custom-set-faces!
`(ahs-face :foreground "White" :background ,(doom-color 'magenta))
`(ahs-face-unfocused :foreground "White" :background ,(doom-color 'magenta))
`(ahs-definition-face :foreground "White" :background ,(doom-color 'magenta))
`(ahs-definition-face-unfocused :foreground "White" :background ,(doom-color 'magenta))
`(ahs-plugin-default-face :inherit hl-line :foreground ,(doom-color 'fg))
`(ahs-plugin-default-face-unfocused :inherit hl-line :foreground ,(doom-color 'magenta)))
Auto themer
Nice way to create custom themes
(package! autothemer)
(use-package! autothemer)
Company
From the
:completion company
module
Auto-complete, yay!
(after! company
(setq company-idle-delay 0.2
company-show-numbers t
company-box-doc-enable nil)
(add-hook! text-mode-hook (setq-local company-idle-delay 1)) ;; Increase the idle-delay for text-modes.
(add-hook 'evil-normal-state-entry-hook #'company-abort)) ;; Makes aborting less annoying.
(setq-default history-length 1000)
Ispell is nice, lets have it in text, markdown, and GFM (Github Flavored Markdown).
(set-company-backend!
'(text-mode markdown-mode gfm-mode)
'(:seperate company-ispell company-files company-yasnippet))
Consult
From the
:completion vertico
module
Since we’re using Marginalia, the separation between buffers and files is already clear, and there’s no need to use a different face.
(after! consult
(set-face-attribute 'consult-file nil :inherit 'consult-buffer)
(setf (plist-get (alist-get 'perl consult-async-split-styles-alist) :initial) ";")
;; Jump to an outline
(map! :leader
(:prefix-map ("s" . "search")
:desc "Jump to outline" "J" #'consult-outline)))
DAP
From the
:tools debugger
module
Debugging using dap-mode
and lsp
brings us breakpoints, a REPL, local variables view for current stack frames and more
to Emacs.
I have not used this too much, so it’s currently at an experimental stage for my part.
Here’s a link to a Systemcrafters Youtube video tutorial on dap-mode
.
(after! dap-mode
;; There's a bug which cause the breakpoint fringe to disappear while
;; the debug session is running. This little hook seems to fix it.
;; https://github.com/emacs-lsp/dap-mode/issues/374#issuecomment-1140399819
(add-hook! +dap-running-session-mode
(set-window-buffer nil (current-buffer)))
;; Doesn't really seem to be working, gotta reopen the buffer...
(add-hook! dap-terminated-hook (set-window-buffer nil (current-buffer)))
;; Windows to be shown when debugging
(setq dap-auto-configure-features '(sessions locals breakpoints expressions controls tooltip)))
Doom emacs doesn’t provides us with out-of-the-box keybindings for now, but there’s an examples in the documentation for now.
(map! :map dap-mode-map
:leader
:prefix ("d" . "dap")
;; basics
:desc "dap next" "n" #'dap-next
:desc "dap step in" "i" #'dap-step-in
:desc "dap step out" "o" #'dap-step-out
:desc "dap continue" "c" #'dap-continue
:desc "dap hydra" "h" #'dap-hydra
:desc "dap debug restart" "r" #'dap-debug-restart
:desc "dap debug" "s" #'dap-debug
:desc "dap disconnect" "q" #'dap-disconnect
;; debug
:prefix ("dd" . "Debug")
:desc "dap debug" "d" #'dap-debug
:desc "dap debug recent" "r" #'dap-debug-recent
:desc "dap debug last" "l" #'dap-debug-last
;; eval
:prefix ("de" . "Eval")
:desc "eval" "e" #'dap-eval
:desc "eval region" "r" #'dap-eval-region
:desc "eval thing at point" "s" #'dap-eval-thing-at-point
:desc "add expression" "a" #'dap-ui-expressions-add
:desc "remove expression" "d" #'dap-ui-expressions-remove
:prefix ("db" . "Breakpoint")
:desc "dap breakpoint toggle" "b" #'dap-breakpoint-toggle
:desc "dap breakpoint condition" "c" #'dap-breakpoint-condition
:desc "dap breakpoint hit count" "h" #'dap-breakpoint-hit-condition
:desc "dap breakpoint log message" "l" #'dap-breakpoint-log-message)
Dired
The dired-recent
package provides us with is a convenient way to visit previously visited directories.
(package! dired-recent) ;; convenient package for visiting frequent folders
(package! dired-open) ;; open files in correct applications
(package! dired-hide-dotfiles) ;; hide or show those pesky .dotfiles
Add some extra configuration options for the dired
file manager.
(after! dired
(dired-async-mode 1)
(dired-recent-mode 1))
(use-package! dired
:config
(require 'evil-collection)
(map! :leader :desc "Dired" "-" #'dired-jump) ;; easy access shortcut
(map! :map dired-recent-mode-map "C-x C-d" nil) ;; hijacks `counsult-dir' command
(evil-collection-define-key 'normal 'dired-mode-map
"h" #'dired-up-directory
"l" #'dired-find-file
"K" #'dired-do-kill-lines ;; This currently conflicts with `+lookup/documentation' keybindings..
"r" #'dired-recent-open
"G" #'consult-dir))
The dired-open
can help us making sure that various files are opened in the correct program. For example,
opening a video file in Emacs is probably not going to be a pleasant experience, and would be better to open
in for example Quicktime or, VLC.
On Linux (or XDG enabled environments), the dired-open
package has a concept of “auto-detecting” which program that should be used to open certain files. I’m mostly using macOS, which does not support XDG very well.
So, we’ll try to write a the dired-open-macos
function to use the native open
program instead.
;; FIXME - Need to make this a bit smarter..
;; I want to open most files in Emacs, and this lets
;; macOS decide a bit too much.
;; (use-package! dired-open
;; :config
;; (add-to-list 'dired-open-functions #'dired-open-macos t)
;; (setq dired-open-extensions '(("svg" . "qlmanage -p"))))
;; (eval-after-load "dired-open"
;; (when (eq system-type 'darwin)
;; '(defun dired-open-macos ()
;; "Try to run `open' to open the file under point as long as the `file' is a regular file,
;; and file extension does not exists in the `dired-open-functions' alist."
;; (interactive)
;; (if (executable-find "open")
;; (let ((file (ignore-errors (dired-get-file-for-visit))))
;; (if (and (f-file-p file) (not (member (file-name-extension file) dired-open-extensions)))
;; (start-process "dired-open" nil
;; "open" (file-truename file)) nil)) nil))))
Especially when visiting my $HOME
directory, it’s littered with all sorts of dot files. Let’s add a convenient
way to show or hide them.
(use-package! dired-hide-dotfiles
:hook (dired-mode . dired-hide-dotfiles-mode)
:config
(evil-collection-define-key 'normal 'dired-mode-map
"H" #'dired-hide-dotfiles-mode))
Since I’m trying to learn using dired
, here’s a small tutorial (based on Systemcrafters ❤) on basic file operations.
File Operations (The tutorial)
Marking files
m
- Marks a fileu
- Unmarks a fileU
- Unmarks all files in buffert
- Inverts marked files in buffer%m
- Marks files in buffer using regular expressions*
- Open menu with lots of auto-marking operationsK
- “Kills” marked items (refresh buffer withg r
to get them back) (Currently conflicts with+lookup/documentation
keybinding)
Many operations can be done on a single file if there are no active marks!
Copying and Renaming files
C
- Copy marked files (or if no files are marked, the current file)R
- Rename marked files, renaming multiple files is a move!% R
- Rename based on regular expression:^test, old-\&
C-x C-q
- Toggle read-only mode. Can edit file names using normal editing functionality. UseZ-Z
to save, changes orZ-Q
to abort.
Deleting files
D
- Delete marked filed
- Mark file for deletionx
- Execute deletion for marks
Creating and extracting archives
Z
- Compress or uncompress a file or folder (to.tar.gz
)c
- Compress selection to a specific file- Pressing
<Enter>
on an archive an preview files inside
Supported compression libraries can be controlled by setting the
dired-compress-files-alist
variable.(after! dired-aux (setq dired-compress-file-alist (append dired-compress-file-alist '(("\\.tar\\.gz\\'" . "tar -cf - %i | gzip -c9 > %o") ("\\.tar\\.bz2\\'" . "tar -cf - %i | bzip -c9 > %o") ("\\.tar\\.xz\\'" . "tar -cf - %i | xz -c9 > %o") ("\\.tar\\.zst\\'" . "tar -cf - %i | zstd -19 -o %o") ("\\.zip\\'" . "zip %o -r --filesync %i")))))
Other common operations
&
- Open file using a sub-process. For example& qlmanage -p
would open the file using macOS quick look.T
- Touch file (update timestamp)M
- Change file modeO
- Change file ownerg G
- Change file groupS
- Create a symbolic link to this fileL
- Load an Emacs Lisp file into EmacsS-<enter>
- Open file in a new windowS-I
- Peek inside folder using thedired-maybe-insert-subdir
function in the same buffer.
Drag-Stuff
I like to drag stuff up and down using C-<up>
and C-<down>
.
(after! drag-stuff
(global-set-key (kbd "<C-up>") #'drag-stuff-up)
(global-set-key (kbd "<C-down>") #'drag-stuff-down))
Evil
From the
:editor evil
module.
Here is a pretty good series on Vim.
Functions
(defun evil-execute-q-macro () "Execute macro stores in q-register, ie. run `@q'." (interactive) (evil-execute-macro 1 "@q")) (defun evil-scroll-to-center-advice (&rest args) "Scroll line to center, for advising functions." (evil-scroll-line-to-center (line-number-at-pos))) (defun evil-end-of-line-interactive () "Wrap `evil-end-of-line' in interactive, fix point being 1+ in vis state." (interactive) (evil-end-of-line)) (defun evil-insert-advice (&rest args) "Tack on after eg. heading insertion for `evil-insert' mode." (evil-insert 1)) (defun evil-scroll-other-window-interactive () "Wrap `scroll-other-window' in interactive." (interactive) (scroll-other-window '-)) (defun evil-scroll-other-window-down-interactive () "Wrap `scroll-other-window-down' in interactive." (interactive) (scroll-other-window)) (defun cust/evil-visual-shift-right () "Wrap `evil-shift-right' in interactive and keeps visual mode" (interactive) (call-interactively 'evil-shift-right) (evil-normal-state) (evil-visual-restore)) (defun cust/evil-visual-shift-left () "Wrap `evil-shift-left' in interactive and keeps visual mode" (interactive) (call-interactively 'evil-shift-left) (evil-normal-state) (evil-visual-restore)) (defun cust/consult-yank-pop () "If there's an active region, delete it before running `consult-yank-pop'" (interactive) (if (use-region-p) (progn (delete-region (region-beginning) (region-end)) (consult-yank-pop)) (consult-yank-pop))) (defun cust/backward-kill-word () "An `Intellij-style' smart backward-kill-word." (interactive) (let* ((cp (point)) (backword) (end) (space-pos) (backword-char (if (bobp) "" ;; cursor in begin of buffer (buffer-substring cp (- cp 1))))) (if (equal (length backword-char) (string-width backword-char)) (progn (save-excursion (setq backword (buffer-substring (point) (progn (forward-word -1) (point))))) (save-excursion (when (and backword ;; when backword contains space (s-contains? " " backword)) (setq space-pos (ignore-errors (search-backward " "))))) (save-excursion (let* ((pos (ignore-errors (search-backward-regexp "\n"))) (substr (when pos (buffer-substring pos cp)))) (when (or (and substr (s-blank? (s-trim substr))) (s-contains? "\n" backword)) (setq end pos)))) (if end (kill-region cp end) (if space-pos (kill-region cp space-pos) (backward-kill-word 1)))) (kill-region cp (- cp 1))) ;; word is non-english word )) (defun cust/forward-kill-word () "An `Intellij-style' smart forward-kill-word." (interactive) (let* ((cp (point)) (forward-word) (end) (space-pos) (forward-word-char (if (eobp) "" ;; cursor at end of buffer (buffer-substring cp (1+ cp))))) (if (equal (length forward-word-char) (string-width forward-word-char)) (progn (save-excursion (setq forward-word (buffer-substring (point) (progn (forward-word 1) (point))))) (save-excursion (when (and forward-word ;; when forward-word contains space (string-match " " forward-word)) (setq space-pos (ignore-errors (search-forward " " nil t))))) (save-excursion (let* ((pos (ignore-errors (search-forward-regexp "\n" nil t))) (substr (when pos (buffer-substring cp pos)))) (when (or (and substr (string-blank-p (string-trim substr))) (string-match "\n" forward-word)) (setq end pos)))) (if end (kill-region cp end) (if space-pos (kill-region cp space-pos) (kill-word 1)))) (kill-region cp (1+ cp))) ;; word is non-English word ))
Keybindings
(after! evil (setq evil-escape-key-sequence "jk") (setq evil-escape-unordered-key-sequence nil) (setq evil-respect-visual-line-mode t) ;; I think the default `backward-kill-word' and `forward-kill-word' functions ;; are a little too greedy. (global-set-key [C-backspace] #'cust/backward-kill-word) (global-set-key [M-backspace] #'cust/backward-kill-word) (global-set-key [C-delete] #'cust/forward-kill-word) (global-set-key [M-delete] #'cust/forward-kill-word) (evil-global-set-key 'normal "Q" #'evil-execute-q-macro) (define-key evil-normal-state-map (kbd "C-S-u") #'evil-scroll-other-window-interactive) (define-key evil-normal-state-map (kbd "C-S-d") #'evil-scroll-other-window-down-interactive) (define-key evil-normal-state-map (kbd "M-y") #'cust/consult-yank-pop) ;; Better "paste" from clipboard (evil-define-key '(visual motion) 'global "H" #'evil-first-non-blank "L" #'evil-end-of-line-interactive "0" #'evil-jump-item) ;; Center text when scrolling and searching for text. (advice-add 'evil-ex-search-next :after #'evil-scroll-to-center-advice) (advice-add 'evil-ex-search-previous :after #'evil-scroll-to-center-advice) (advice-add 'evil-scroll-up :after #'evil-scroll-to-center-advice) (advice-add 'evil-scroll-down :after #'evil-scroll-to-center-advice))
Sometimes it’s convenient to insert multiple cursors using the mouse. Inserts a new cursor using
C-S-<mouse-1>
.(after! evil (global-set-key (kbd "C-S-<mouse-1>") #'evil-mc-toggle-cursor-on-click))
Folding
From the
:editor fold
module
Doom Emacs comes with a combination of hideshow
, vimish-fold
and outline-minor-mode
to
enable folding for various modes. It doesn’t seem to work properly with LSP
mode, so we’ll
configure lsp-origami
to use for code folding with lsp-mode
.
(package! lsp-origami)
(use-package! lsp-origami
:hook (lsp-after-open-hook . #'lsp-origami-try-enable))
Formatting
From the
:editor format
module
Doom Emacs now uses aphelieia as the default formatting system. Override to use the latest version from Github.
(package! apheleia :recipe (:repo "radian-software/apheleia"))
GitHub Copilot
Unofficial GitHub Copilot plugin for Emacs.
(package! copilot
:recipe (:host github :repo "zerolfx/copilot.el" :files ("*.el" "dist")))
;; accept completion from copilot and fallback to company
(use-package! copilot
:hook (prog-mode . copilot-mode)
:bind (:map copilot-completion-map
("<tab>" . 'copilot-accept-completion)
("TAB" . 'copilot-accept-completion)
("C-TAB" . 'copilot-accept-completion-by-word)
("C-<tab>" . 'copilot-accept-completion-by-word))
:config (add-to-list 'copilot--indentation-alist '(elixir-mode elixir-smie-indent-basic)))
Required to run M-x copilot-login
for using the plugin.
Golden Ratio
When working with multiple windows, each window has a size that’s not necessarily convenient for editing.
The golden-ratio-mode
automatically adjusts the size of the “active” window to the size specified in the “Golden Ratio”.
(package! golden-ratio :recipe (:host github :repo "roman/golden-ratio.el" :files ("*.el")))
;; (use-package! golden-ratio
;; :defer t
;; :hook (after-init . golden-ratio-mode)
;; :custom (golden-ratio-exclude-modes '(occur-mode))
;; :config (add-hook 'doom-switch-window-hook #'golden-ratio))
Spelling
Easily cycle between the languages I use (English and Norwegian) by hitting F8
.
(let ((langs '("en" "no")))
(setq lang-ring (make-ring (length langs)))
(dolist (elem langs) (ring-insert lang-ring elem)))
(defun cycle-ispell-languages ()
"Cycle between languages in the language ring `lang-ring`."
(interactive)
(let ((lang (ring-ref lang-ring -1)))
(ring-insert lang-ring lang)
(ispell-change-dictionary lang)))
;; Cycle languages with F8
(global-set-key (kbd "<f8>") #'cycle-ispell-languages)
;; And add a Doom shortcut to the "toggle" menu for good measures
(map! :leader
(:prefix-map ("t" . "toggle")
:desc "Next spell checker language" "S" #'cycle-ispell-languages))
For a personal dictionaries (with all the misspelled words and such)
(setq ispell-extra-args '("--sug-mode=ultra" "--run-together")
ispell-local-dictionary "en"
ispell-program-name "aspell"
ispell-personal-dictionary (expand-file-name ".dictionaries/personal.pws" doom-private-dir))
(unless (file-exists-p ispell-personal-dictionary)
(write-region "" nil ispell-personal-dictionary nil 0))
(use-package! flyspell
:when (modulep! :checkers spell +flyspell)
:after ispell
;; :hook (find-file . flyspell-on-for-buffer-type) ;; Getting errors, needs fix
:init
(defun flyspell-on-for-buffer-type ()
"Enable Flyspell appropriately for the major mode of the current buffer.
Uses `flyspell-prog-mode' for modes derived from `prog-mode', so only strings
and comments get checked. Other buffers get `flyspell-mode' to check all text.
If flyspell is already enabled for buffer, do nothing."
(interactive)
(if (not (symbol-value flyspell-mode)) ;; if not already enabled
(progn
(if (derived-mode-p 'prog-mode)
(progn
(message "Flyspell on (code)")
(flyspell-prog-mode))
(progn ;; else
(message "Flyspell on (text)")
(flyspell-mode 1))))))
:config
;; Significantly speeds up flyspell, which would otherwise print
;; messages for every word when checking the entire buffer
(setq flyspell-issue-welcome-flag nil
flyspell-issue-message-flag nil))
Downloading dictionaries
Currently using Aspell with Norwegian and English languages. Should probably document installation.
Intility Command Line Tool (inctl)
;; (use-package! inctl
;; :load-path (expand-file-name "lisp/inctl" doom-private-dir)
;; :config inctl-dispatch)
Language tool
From the
:checkers grammar
module
This seems pretty neat, so I’ll experiment a bit with it. Some issues I’d like to see resolved though:
- Don’t check org src blocks
- Dont check org headers and properties
- Look into running the https server to avvoid JVM spinup every time I check grammars
- Check grammar at point - now I need to run
langtool-correct-buffer
and chose from there, would be nice to just correct what’s at point.
Installation
Requires installation of
languagetool
which can easily be installed with homebrew on macOS.$ brew install languagetool
Configuration
Next, we just need to point at the
languagetool-commandline.jar
file. Easily found somewhere in the$HOMEBREW_PREFIX
directory. Should probably have a better way to point at this though.$ find $HOMEBREW_PREFIX -name languagetool-commandline.jar /usr/local/Cellar/languagetool/5.6/libexec/languagetool-commandline.jar
(defun langtool-autoshow-detail-popup (overlays) (when (require 'popup nil t) ;; Do not interrupt current popup (unless (or popup-instances ;; suppress popup after type `C-g` . (memq last-command '(keyboard-quit))) (let ((msg (langtool-details-error-message overlays))) (popup-tip msg)))))
(after! writegood-mode (if (file-exists-p "/usr/local/Cellar/languagetool/5.6/libexec/languagetool-commandline.jar") (setq langtool-language-tool-jar "/usr/local/Cellar/languagetool/5.6/libexec/languagetool-commandline.jar") (setq langtool-autoshow-message-function #'langtool-autoshow-detail-popup)) ;; Ignoring these rules makes org files behave a little nicer (setq langtool-disabled-rules '("WHITESPACE_RULE" "MORFOLOGIK_RULE_EN_US" "DOUBLE_PUNCTUATION" "COMMA_PARENTHESIS_WHITESPACE")))
Lorem-Ipsum
Sometimes I need some example text to work on, use as placement holders and so on.
(package! lorem-ipsum)
(use-package! lorem-ipsum
:config (lorem-ipsum-use-default-bindings))
Default keybindings for this package is:
C-c l s
- Insert sentencesC-c l p
- Insert paragraphsC-c l l
- Insert lists
Version Control System
Magit
From the
:tools magit
moduleTaken from the Modern Emacs blog
(after! magit (defvar pretty-magit--alist nil "An alist of regexes, an icon, and face properties to apply to icon.") (defvar pretty-magit--prompt nil "A list of commit leader prompt candidates.") (defvar pretty-magit--use-commit-prompt? nil "Do we need to use the magit commit prompt?") (defun pretty-magit--add-magit-faces () "Add face properties and compose symbols for buffer from pretty-magit." (interactive) (with-silent-modifications (-each pretty-magit--alist (-lambda ((rgx char face-props)) (save-excursion (goto-char (point-min)) (while (re-search-forward rgx nil t) (-let [(start end) (match-data 1)] (compose-region start end char) (when face-props (add-face-text-property start end face-props))))))))) (defun pretty-magit-add-leader (word char face-props) "Replace sanitized WORD with CHAR having FACE-PROPS and add to prompts." (add-to-list 'pretty-magit--alist (list (rx-to-string `(: bow (group ,word ":"))) char face-props)) (add-to-list 'pretty-magit--prompt (concat word ": "))) (defun pretty-magit-add-leaders (leaders) "Map `pretty-magit-add-leader' over LEADERS." (-each leaders (-applify #'pretty-magit-add-leader))) (defun pretty-magit--use-commit-prompt (&rest args) (setq pretty-magit--use-commit-prompt? t)) (defun pretty-magit-commit-prompt () "Magit prompt and insert commit header with faces." (interactive) (when (and pretty-magit--use-commit-prompt? pretty-magit--prompt) (setq pretty-magit--use-commit-prompt? nil) (insert (completing-read "Commit Type " pretty-magit--prompt nil 'confirm)) (pretty-magit--add-magit-faces) (evil-insert 1))) (defun pretty-magit-setup (&optional no-commit-prompts?) "Advise the appropriate magit funcs to add pretty-magit faces." (advice-add 'magit-status :after 'pretty-magit--add-magit-faces) (advice-add 'magit-refresh-buffer :after 'pretty-magit--add-magit-faces) (unless no-commit-prompts? (remove-hook 'git-commit-setup-hook 'with-editor-usage-message) (add-hook 'git-commit-setup-hook 'pretty-magit-commit-prompt) (advice-add 'magit-commit-create :after 'pretty-magit--use-commit-prompt))))
(after! magit (pretty-magit-add-leaders '(("feat" ? (:foreground "#C2C8CD" :height 1.2)) ("add" ? (:foreground "#375E97" :height 1.2)) ("fix" ? (:foreground "#FB6542" :height 1.2)) ("clean" ? (:foreground "#FFBB00" :height 1.2)) ("chore" ? (:foreground "#CE98FF" :height 1.2)) ("docs" ? (:foreground "#3F681C" :height 1.2)))) (pretty-magit-setup))
Run
vc-refresh-state()
for all buffers in Magit’spost-refresh-hook
.(after! magit (defun cust/vc-refresh-all-buffers-state () "Update version control state in all buffers" (dolist (buf (buffer-list)) (with-current-buffer buf (vc-refresh-state)))) (add-hook 'magit-post-refresh-hook #'cust/vc-refresh-all-buffers-state))
When copying commit hashes I almost always just want the short version. This is controlled by the
magit-copy-revision-abbreviated
variable. The latest commit hash can be copied from a magit buffer usingM-w
.(after! magit (setq magit-copy-revision-abbreviated t ;; Copy short version of hashes magit-list-refs-sortby "-committerdate")) ;; Sort by last commited date (latest on top)
Custom Remotes
;;; Forge (after! forge (setq gitlab.user "user") (add-to-list 'forge-alist '("gitlab.intility.com" "gitlab.intility.com/api/v4" "gitlab.intility.com" forge-gitlab-repository))) ;;; Browse remotes (require 'browse-at-remote) (add-to-list 'browse-at-remote-remote-type-regexps '("^gitlab\\.intility\\.com$" . "gitlab"))
Markdown-XWidget
A tool that uses xwidget
to preview markdown files.
This doesn’t seem to work to good at the moment, giving error:
executable-find: Wrong type argument: stringp, +markdown-compile
(package! markdown-xwidget
:recipe (:host github
:repo "cfclrk/markdown-xwidget"
:files (:defaults "resources")))
(use-package! markdown-xwidget
:after markdown-mode
:init
(map! :map markdown-mode-map
:localleader
"p" #'markdown-xwidget-preview-mode))
Marginalia
From the
:completion vertico
module
Marginalia is a tool (written by the same author as vertico) which adds marginalia to the mini-buffer completions. Marginalia are marks and annotations placed at the margin of a page in a book.
The below settings are basically just copied of tecosaur’s config, and makes it look a bit nicer.
- Add color to file attributes
- Don’t display user:group information if I am the owner/group
- When a file modified time is quite recent, use relative age (eg.
2h ago
) - Add fatter font face for bigger files
(after! marginalia
(setq marginalia-censor-variables nil)
(defadvice! +marginalia--anotate-local-file-colorful (cand)
"Just a more colourful version of `marginalia--anotate-local-file'."
:override #'marginalia--annotate-local-file
(when-let (attrs (file-attributes (substitute-in-file-name
(marginalia--full-candidate cand))
'integer))
(marginalia--fields
((marginalia--file-owner attrs)
:width 12 :face 'marginalia-file-owner)
((marginalia--file-modes attrs))
((+marginalia-file-size-colorful (file-attribute-size attrs))
:width 7)
((+marginalia--time-colorful (file-attribute-modification-time attrs))
:width 12))))
(defun +marginalia--time-colorful (time)
(let* ((seconds (float-time (time-subtract (current-time) time)))
(color (doom-blend
(face-attribute 'marginalia-date :foreground nil t)
(face-attribute 'marginalia-documentation :foreground nil t)
(/ 1.0 (log (+ 3 (/ (+ 1 seconds) 345600.0)))))))
;; 1 - log(3 + 1/(days + 1)) % grey
(propertize (marginalia--time time) 'face (list :foreground color))))
(defun +marginalia-file-size-colorful (size)
(let* ((size-index (/ (log10 (+ 1 size)) 7.0))
(color (if (< size-index 10000000) ; 10m
(doom-blend 'orange 'green size-index)
(doom-blend 'red 'orange (- size-index 1)))))
(propertize (file-size-human-readable size) 'face (list :foreground color)))))
Outshine
Outshine attempts to bring the look and feel of org-mode
to the world outside Org major mode.
It’s an extension of outline-minor-mode
that should act as a replacement of outline-mode
.
(package! outshine)
(use-package! outshine
:hook ((prog-mode . outline-minor-mode)
(outline-minor-mode . outshine-mode))
:init
(progn
(advice-add 'outshine-narrow-to-subtree :before #'outshine-fix-narrow-pos)
(advice-add 'outshine-insert-heading :before #'outshine-fix-insert-pos)
(advice-add 'outshine-insert-heading :after #'evil-insert-advice)
(advice-add 'outshine-insert-subheading :after #'evil-insert-advice))
:config
(evil-collection-define-key '(normal visual motion) 'outline-minor-mode-map
"gh" #'outline-up-heading
"gj" #'outline-forward-same-level
"gk" #'outline-backward-same-level
"gl" #'outline-next-visible-heading
"gu" #'outline-previous-visible-heading))
Pinentry
Emacs pinentry integration
(package! pinentry)
Projectile
From the
:core packages
module
Set some sensible projectile
settings.
(setq projectile-enable-caching t
projectile-project-search-path '("~/workspace") ;; A relic directory from when I used Eclipse back in the days
projectile-globally-ignored-files '(".DS_Store") ;; Super annoying files
projectile-globally-ignored-directories '(".git" ;; I never want to cache files in these directories
".idea"
".import"
".elixir_ls"
".htmlcov"
".pytest_cache"
"_build"
"__pycache__"
"deps"
"node_modules"))
Smartparens
From the
:core packages
module.
(after! smart-parens
(sp-local-pair '(org-mode) "<<" ">>" :actions '(insert)))
Enable colored matching for delimiters
(add-hook! 'prog-mode-hook #'rainbow-delimiters-mode)
Tramp
Tramp makes accessing remote file systems using Emacs a blast.
(after! tramp
(setq tramp-default-method "scp"))
Treemacs
Make treemacs
pretty and functional.
(after! (treemacs winum)
(setq doom-themes-treemacs-theme "doom-colors" ; Enable nice colors for treemacs
doom-themes-treemacs-enable-variable-pitch t) ; Enable variable-pitch font
(setq winum-ignored-buffers-regexp
(delete (regexp-quote (format "%sFramebuffer-" treemacs--buffer-name-prefix))
winum-ignored-buffers-regexp))
;; This PR cause treemacs to appear on the bottom instead of on the left/right hand side
;; https://github.com/Alexander-Miller/treemacs/pull/971
;; Workaround:
(set-popup-rule! "^ \\*Treemacs"
:side treemacs-position
:width treemacs-width
:quit nil)
(treemacs-project-follow-mode t)
(treemacs-follow-mode t)
(treemacs-define-RET-action 'file-node-open #'treemacs-visit-node-in-most-recently-used-window)
(treemacs-define-RET-action 'file-node-closed #'treemacs-visit-node-in-most-recently-used-window))
lsp-treemacs
integrates treemacs
with lsp-mode
.
(after! (treemacs lsp-mode)
(lsp-treemacs-sync-mode 1))
(map! :leader
:after lsp-treemacs
(:prefix-map ("c" . "code")
:desc "List errors" "x" #'lsp-treemacs-errors-list
:desc "Show symbols" "y" #'lsp-treemacs-symbols))
Vertico
From the
:completion vertico
module
Vertico is the new kid on the block when it comes to searching in Emacs. Built on top of the native Emacs search APIs instead of 3rd party tools.
(after! vertico
;; Emacs 28+: Hide commands in M-x that doesn't work in the current mode.
;; Vertico commands are hidden in normal buffers.
(setq read-extended-command-predicate #'command-completion-default-include-p)
;; These mappings seems a bit inverted, but it works as I like it, so I dunno...?
(map! :map vertico-map
"C-u" #'vertico-scroll-down
"C-d" #'vertico-scroll-up
"C-c C-o" #'embark-export))
Tips’n tricks
- Sometimes you want to input some value that’s similar to a pre-selected choice. We can use the input
value by hitting
M-<enter>
to use our input value instead of the pre-selected value.
Vertico Posframe (experimental)
I’ve seen some cool stuff using a separate
posframe
for displayingvertico
results. I’m still not convinced, because it tends to hide the content of my buffers when displayed on top of it. Either case; it can be enabled by installing thevertico-posframe
package.(package! vertico-posframe)
And enabled like this.
(after! vertico (vertico-posframe-mode 1))
Very large files
The very large files mode loads large files in chunks, letting us open ridiculously large files!
(package! vlf
:recipe (:host github :repo "m00natic/vlfi" :files ("*.el") :branch "master"))
We usually don’t need this package right away, so we’ll delay the loading a bit.
(use-package! vlf-setup
:defer-incrementally vlf-tune vlf-base vlf-write vlf-search vlf-occur vlf-follow vlf-ediff vlf)
VTerm
From the
:term vterm
module
The vterm
gives us native performing terminal emulation inside Emacs; what’s not to like?
TODO - Annoying stuff that doesn’t work in vterm
<delete>
doesn’t delete words forwardsM-<backspace>
doesn’t delete words backwards- Can’t type
M
??
(map! :after vterm
:map vterm-mode-map
;; "M-<backspace>" #'vterm-send-meta-backspace
;; "M-<delete>" #'vterm-send-delete
;; Needs to bind these here as well for some reason..
"M-0" #'treemacs-select-window
"M-1" #'winum-select-window-1
"M-2" #'winum-select-window-2
"M-3" #'winum-select-window-3
"M-4" #'winum-select-window-4
"M-5" #'winum-select-window-5
"M-6" #'winum-select-window-6
"M-7" #'winum-select-window-7
"M-8" #'winum-select-window-8
"M-9" #'winum-select-window-9)
YASnippet
From the
:editor snippets
module.
Enable nested snippets.
(setq yas-triggers-in-field t)
Extra snippets Snippets are basic text files (no extension), located in directories representing the mode they will be active for. For a quick guide on how we can organize snippets, take a look at this page.
.
|-- c-mode
| -- printf
|-- elixir-mode
| -- defmodule
| -- macro
|-- org-mode
-- header-props
-- and-so-on
I like to keep my custom stuff in the misc directory.
(setq +snippets-dir (concat (file-name-as-directory doom-private-dir) "misc/snippets"))
Visuals
Info colors
This makes manual pages look pretty by adding variable pitch fortification and colors 🤗
(package! info-colors)
To use this we’ll just hook into Info
.
(use-package! info-colors
:after info
:hook (Info-selection-hook . info-colors-fontify-node)
:commands (info-colors-fontify-node))
Writeroom / Zen mode
From the
:ui zen
module.
I like to toggle writeroom-mode
for all buffers at once.
(map! :leader
(:when (modulep! :ui zen)
(:prefix-map ("t" . "toggle")
:desc "Zen mode" "z" #'global-writeroom-mode)))
Slightly decrease the default text scale for writeroom-mode
.
(setq +zen-text-scale 0.8)
When using Writeroom mode with Org, make some additional aesthetic tweaks:
- Use a serifed variable-pitch font
- Hide headline leading stars
- Nicer headline bullets
- Hide line numbers
- Remove outline indentation
- Centering the text
- Turn on
org-pretty-table-mode
(defvar +zen-serif-p t
"Wheter to use a serifed font with `mixed-pitch-mode'.")
(after! writeroom-mode
(defvar-local +zen--original-org-indent-mode-p nil)
(defvar-local +zen--original-mixed-pitch-mode-p nil)
(defvar-local +zen--original-org-pretty-table-mode-p nil)
(defun +zen-enable-mixed-pitch-mode-h ()
"Enable `mixed-pitch-mode' when in `+zen-mixed-pitch-modes'."
(when (apply #'derived-mode-p +zen-mixed-pitch-modes)
(if writeroom-mode
(progn
(setq +zen--original-mixed-pitch-mode-p mixed-pitch-mode)
(funcall (if +zen-serif-p #'mixed-pitch-serif-mode #'mixed-pitch-mode) 1))
(funcall #'mixed-pitch-mode (if +zen--original-mixed-pitch-mode-p 1 -1)))))
(pushnew! writeroom--local-variables
'display-line-numbers
'visual-fill-column-width
'org-adapt-indentation
'org-superstar-headline-bullets-list
'org-superstar-remove-leading-stars)
(add-hook 'writeroom-mode-enable-hook
(defun +zen-prose-org-h ()
"Reformat the current Org buffer appearance for prose."
(when (eq major-mode 'org-mode)
(setq display-line-numbers nil
visual-fill-column-width 60
org-adapt-indentation nil)
(when (featurep 'org-superstar)
(setq-local org-superstar-headline-bullets-list '("🙘" "🙙" "🙚" "🙛")
;; org-superstar-headline-bullets-list '("🙐" "🙑" "🙒" "🙓" "🙔" "🙕" "🙖" "🙗")
org-superstar-remove-leading-stars t)
(org-superstar-restart))
(setq
+zen--original-org-indent-mode-p org-indent-mode
+zen--original-org-pretty-table-mode-p (bound-and-true-p org-pretty-table-mode))
(org-indent-mode -1)
(org-pretty-table-mode 1))))
(add-hook 'writeroom-mode-disable-hook
(defun +zen-nonprose-org-h ()
"Reverse the effect of `+zen-prose-org'."
(when (eq major-mode 'org-mode)
(when (featurep 'org-superstar)
(org-superstar-restart))
(when +zen--original-org-indent-mode-p (org-indent-mode 1))
;; (unless +zen--original-org-pretty-table-mode-p (org-pretty-table-mode -1))
;; TODO disable ahs-mode
))))
Highlight Indent Guides
The highlight-indent-guides minor mode package can be useful in certain prog-modes
.
(add-hook! 'prog-mode-hook #'highlight-indent-guides-mode)
(setq highlight-indent-guides-method #'character)
Language configurations
General
General configuration
File templates
For some file types, we overwrite defaults in the snippets directory, others need to have a template assigned.
;; (set-file-template! "\\.tex$" :trigger "__" :mode 'latex-mode)
(set-file-template! "\\.org$" :trigger "__" :mode 'org-mode)
(set-file-template! "/LICEN[CS]E$" :trigger '+file-templates/insert-license)
LSP
(after! lsp-mode
(setq read-process-output-max (* 1024 1024) ;; 1mb
lsp-auto-guess-root t
lsp-file-watch-threshold 1000000
lsp-idle-delay 0.500
lsp-modeline-code-actions-segments '(count icon name)
lsp-response-timeout 10))
(after! lsp-ui
(define-key lsp-ui-mode-map [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
(define-key lsp-ui-mode-map [remap xref-find-references] #'lsp-ui-peek-find-references))
Emacs LSP Performance Booster
Emacs LSP Performance Booster is a wrapper executable written in Rust that improves the performance of LSP mode. See build instructions in the Github repository.
(defun lsp-booster--advice-json-parse (old-fn &rest args) "Try to parse bytecode instead of json." (or (when (equal (following-char) ?#) (let ((bytecode (read (current-buffer)))) (when (byte-code-function-p bytecode) (funcall bytecode)))) (apply old-fn args))) (advice-add (if (progn (require 'json) (fboundp 'json-parse-buffer)) 'json-parse-buffer 'json-read) :around #'lsp-booster--advice-json-parse) (defun lsp-booster--advice-final-command (old-fn cmd &optional test?) "Prepend emacs-lsp-booster command to lsp CMD." (let ((orig-result (funcall old-fn cmd test?))) (if (and (not test?) ;; for check lsp-server-present? (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper lsp-use-plists (not (functionp 'json-rpc-connection)) ;; native json-rpc (executable-find "emacs-lsp-booster")) (progn (message "Using emacs-lsp-booster for %s!" orig-result) (cons "emacs-lsp-booster" orig-result)) orig-result))) (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)
Yaml
Yaml comes in many different flavors, let’s select schema with ,s
.
(map! :map yaml-mode-map
:localleader
:desc "Select buffer schema" "s" #'lsp-yaml-select-buffer-schema)
Plaintext
It’s nice to see ANSI colour codes displayed. However, until Emacs 28 it’s not possible to do this without modifying the buffer, so let’s condition this block on that.
(after! text-mode
(add-hook! 'text-mode-hook
;; Apply ANSI color codes
(with-silent-modifications
(ansi-color-apply-on-region (point-min) (point-max) t))))
Org
Everybody likes org-mode!
(setq org-directory "~/Dropbox/org"
org-cliplink-transport-implementation 'curl
org-crypt-key "rhblind@gmail.com"
org-tag-alist '(("crypt" . ?c))
org-id-link-to-org-use-id t
org-agenda-text-search-extra-files '())
(setq org-download-image-dir (concat (file-name-as-directory org-directory) "images"))
Behaviour
Sometimes I’m a bit lazy and just want to click around with the mouse.
(require 'org-mouse)
Keybindings
Add some handy keybindings under
SPC-, x
for emphasizing text inorg-mode
.(after! org (defmacro cust/org-emphasize (fname char) "Macro that creates a function for setting emphasises in org-mode" `(defun ,fname () (interactive) (org-emphasize ,char))))
(map! :map org-mode-map :after org :localleader "x" nil ;; raises Key sequence error when trying to rebind below unless unset first. :desc "org-toggle-checkbox" "X" #'org-toggle-checkbox) ;; rebind org-toggle-checkbox to "X" (map! :map org-mode-map :after org :localleader (:prefix ("x" . "text") :desc "bold" "b" (cust/org-emphasize cust/org-emphasize-bold ?*) :desc "code" "c" (cust/org-emphasize cust/org-emphasize-code ?~) :desc "italic" "i" (cust/org-emphasize cust/org-emphasize-italic ?/) :desc "clear" "r" (cust/org-emphasize cust/org-emphasize-clear ? ) :desc "strike-through" "s" (cust/org-emphasize cust/org-emphasize-strike-through ?+) :desc "underline" "u" (cust/org-emphasize cust/org-emphasize-underline ?_) :desc "verbatim" "v" (cust/org-emphasize cust/org-emphasize-verbatim ?=)))
Babel
Org babel is used to evaluate code blocks in org files.
(after! org
(setq org-confirm-babel-evaluate nil
org-src-fontify-natively t
org-src-tab-acts-natively t
org-src-preserve-indentation t
org-src-window-setup 'current-window
org-babel-default-header-args '((:session . "none")
(:results . "replace")
(:exports . "code")
(:cache . "no")
(:noweb . "no")
(:hlines . "no")
(:tangle . "no")
(:comment . "link")))
(add-to-list 'org-babel-load-languages '(dot . t)))
Sometimes it’s nice to auto-format code blocks.
(after! org
(defun org-indent-src-block ()
"Indent the source block at point."
(interactive)
(when (org-in-src-block-p)
(org-edit-special)
(indent-region (point-min) (point-max))
(org-edit-src-exit))))
Reveal export
By default reveal is rather nice, there are just a few tweaks that I consider a good idea.
(setq org-re-reveal-theme "white"
org-re-reveal-transition "slide"
org-re-reveal-plugins '(markdown notes math search zoom))
Hugo (Blog)
From the
:lang org +hugo
module
Install the hugo
blog engine using homebrew, and set the org-hugo-base-dir
variable.
(We can also set the HUGO_BASE_DIR
environmental variable).
- [[https://spcbfr.vercel.app/posts/blogging-setup-hugo-and-org][How to make a blog with hugo and org-mode
- Youssef Bouzekri — A developer w…]]
Org-mode configuration for Emacs - Hugo Cisneros
(when (and (modulep! :lang org +hugo) (eq system-type 'darwin))
(setq org-hugo-base-dir (concat (expand-file-name (file-name-as-directory "~")) "workspace/hugo-blog")))
Create a new blog
$ brew install hugo
$ hugo new site ~/Dropbox/org/hugo-blog
Roam
From the
:lang org +roam
module
(setq org-roam-v2-ack t
org-roam-directory (concat (file-name-as-directory org-directory) "roam"))
(setq org-roam-index-file (concat (file-name-as-directory org-roam-directory) "index.org"))
Make sure roam is available on startup.
(org-roam-db-autosync-mode)
Add all org-roam
files to list of extra files to be searched by text commands.
(after! (org org-roam)
(setq org-agenda-text-search-extra-files (org-roam--list-files org-roam-directory)))
Visuals
Tables
Org tables aren’t the prettiest thing to look at. This package is supposed to redraw them in the buffer with box-drawing characters. Sounds like an improvement to me! We’ll make use of this with writeroom-mode.
(package! org-pretty-table :recipe (:host github :repo "Fuco1/org-pretty-table" :branch "master"))
(use-package! org-pretty-table :hook (org-mode . org-pretty-table-mode) :commands (org-pretty-table-mod global-org-pretty-table-mode))
Emphasis markers
While org-hide-emphasis-markers is very nice, it can sometimes make edits which occur at the border a bit more fiddley. We can improve this situation without sacrificing visual amenities with the org-appear package.
(package! org-appear :recipe (:host github :repo "awth13/org-appear"))
(use-package! org-appear :hook (org-mode . org-appear-mode) :config (setq org-appear-autoemphasis t org-appear-autosubmarkers t org-appear-autolinks nil) ;; for proper first-time setup, `org-appear--set-elements' ;; needs to be run after other hooks have acted. (run-at-time nil nil #'org-appear--set-elements))
Symbols
This section is pretty much just copied from tecosaurs config (again…)
(after! org-superstar (setq org-superstar-headline-bullets-list '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶") org-superstar-prettify-item-bullets t )) (setq org-ellipsis " ▾ " org-hide-leading-stars t org-priority-highest ?A org-priority-lowest ?E org-priority-faces '((?A . 'all-the-icons-red) (?B . 'all-the-icons-orange) (?C . 'all-the-icons-yellow) (?D . 'all-the-icons-green) (?E . 'all-the-icons-blue)))
It’s also nice to make use of the Unicode characters for check boxes, and other commands.
(appendq! +ligatures-extra-symbols `(:checkbox "☐" :pending "◼" :checkedbox "☑" :list_property "∷" :em_dash "—" :ellipses "…" :arrow_right "→" :arrow_left "←" :title "𝙏" :subtitle "𝙩" :author "𝘼" :date "𝘿" :property "☸" :options "⌥" :startup "⏻" :macro "𝓜" :html_head "🅷" :html "🅗" :latex_class "🄻" :latex_header "🅻" :beamer_header "🅑" :latex "🅛" :attr_latex "🄛" :attr_html "🄗" :attr_org "⒪" :begin_quote "❝" :end_quote "❞" :caption "☰" :header "›" :results "🠶" :begin_export "⏩" :end_export "⏪" :properties "⚙" :end "∎" :priority_a ,(propertize "⚑" 'face 'all-the-icons-red) :priority_b ,(propertize "⬆" 'face 'all-the-icons-orange) :priority_c ,(propertize "■" 'face 'all-the-icons-yellow) :priority_d ,(propertize "⬇" 'face 'all-the-icons-green) :priority_e ,(propertize "❓" 'face 'all-the-icons-blue))) (set-ligatures! 'org-mode :merge t :checkbox "[ ]" :pending "[-]" :checkedbox "[X]" :list_property "::" :em_dash "---" :ellipsis "..." :arrow_right "->" :arrow_left "<-" :title "#+title:" :subtitle "#+subtitle:" :author "#+author:" :date "#+date:" :property "#+property:" :options "#+options:" :startup "#+startup:" :macro "#+macro:" :html_head "#+html_head:" :html "#+html:" :latex_class "#+latex_class:" :latex_header "#+latex_header:" :beamer_header "#+beamer_header:" :latex "#+latex:" :attr_latex "#+attr_latex:" :attr_html "#+attr_html:" :attr_org "#+attr_org:" :begin_quote "#+begin_quote" :end_quote "#+end_quote" :caption "#+caption:" :header "#+header:" :begin_export "#+begin_export" :end_export "#+end_export" :results "#+RESULTS:" :property ":PROPERTIES:" :end ":END:" :priority_a "[#A]" :priority_b "[#B]" :priority_c "[#C]" :priority_d "[#D]" :priority_e "[#E]") (plist-put +ligatures-extra-symbols :name "⁍")
Font Display
Mixed pitch is pretty great, and so is
+org-pretty-mode
. Use them together!(add-hook! 'org-mode-hook #'+org-pretty-mode)
Make headings slightly bigger
(custom-set-faces! '(outline-1 :weight extra-bold :height 1.25) '(outline-2 :weight bold :height 1.15) '(outline-3 :weight bold :height 1.12) '(outline-4 :weight semi-bold :height 1.09) '(outline-5 :weight semi-bold :height 1.06) '(outline-6 :weight semi-bold :height 1.03) '(outline-8 :weight semi-bold) '(outline-9 :weight semi-bold))
And the same with the title.
(custom-set-faces! '(org-document-title :height 1.2))
It seems reasonable to have deadlines in the error face when they’re passed.
(setq org-agenda-deadline-faces '((1.001 . error) (1.0 . org-warning) (0.5 . org-upcoming-deadline) (0.0 . org-upcoming-distant-deadline)))
We can then have quote blocks stand out a bit more by making them italic.
(setq org-fontify-quote-and-verse-blocks t)
Super agenda
A super agenda
(package! org-super-agenda)
(use-package! org-super-agenda :commands org-super-agenda-mode)
(after! org-agenda (org-super-agenda-mode)) (setq org-agenda-breadcrumbs-separator " ❱ " org-agenda-compact-blocks t org-agenda-include-deadlines t ;; Include deadlines in the agenda org-agenda-skip-deadline-if-done t ;; Don't include deadlines in the agenda if they're in the `DONE' state org-agenda-skip-scheduled-if-done t org-agenda-skip-scheduled-if-done t ;; Don't include items in the agenda if they're in the `DONE' state org-agenda-tags-column 100 ;; from testing this seems to be a good value org-super-agenda-header-map nil ;; Fixes issues with evil-mode org-agenda-block-separator 9472) ;; Use a straight line as separator between agenda agenda blocks (setq org-agenda-custom-commands '(("o" "Overview" ((agenda "" ((org-agenda-span 'day) (org-super-agenda-groups '((:name "Today" :time-grid t :date today :todo "TODAY" :scheduled today :order 1))))) (alltodo "" ((org-agenda-overriding-header "") (org-super-agenda-groups '((:name "Next to do" :todo "NEXT" :order 1) (:name "Important" :tag "Important" :priority "A" :order 6) (:name "Due Today" :deadline today :order 2) (:name "Due Soon" :deadline future :order 8) (:name "Overdue" :deadline past :face error :order 7) (:name "Work" :tag "Work" :order 10) (:name "Issues" :tag "Issue" :order 12) (:name "Emacs" :tag "Emacs" :order 13) (:name "Projects" :tag "Project" :order 14) (:name "Research" :tag "Research" :order 15) (:name "To read" :tag "Read" :order 30) (:name "Waiting" :todo "WAITING" :order 20) (:name "University" :tag "uni" :order 32) (:name "Trivial" :priority<= "E" :tag ("Trivial" "Unimportant") :todo ("SOMEDAY" ) :order 90) (:discard (:tag ("Chore" "Routine" "Daily")))))))))))
Capture templates (DOCT)
(package! doct) (package! doct-org-roam :recipe (:host nil :type git :repo "https://gist.github.com/f9b21eeea7d7c9123dc400a30599d50d.git" :files ("doct-org-roam.el")))
(use-package! doct :commands doct)
Visuals
This piece of code improves how the capturing templates look.
(after! org-capture (defun org-capture-select-template-prettier (&optional keys) "Select a capture template, in a prettier way than default Lisp programs can force the template by setting KEYS to a string." (let ((org-capture-templates (or (org-contextualize-keys (org-capture-upgrade-templates org-capture-templates) org-capture-templates-contexts) '(("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n %u\n %a"))))) (if keys (or (assoc keys org-capture-templates) (error "No capture template referred to by \"%s\" keys" keys)) (org-mks org-capture-templates "Select a capture template\n━━━━━━━━━━━━━━━━━━━━━━━━━" "Template key: " `(("q" ,(concat (all-the-icons-octicon "stop" :face 'all-the-icons-red :v-adjust 0.01) "\tAbort"))))))) (advice-add 'org-capture-select-template :override #'org-capture-select-template-prettier) (defun org-mks-pretty (table title &optional prompt specials) "Select a member of an alist with multiple keys. Prettified. TABLE is the alist which should contain entries where the car is a string. There should be two types of entries. 1. prefix descriptions like (\"a\" \"Description\") This indicates that `a' is a prefix key for multi-letter selection, and that there are entries following with keys like \"ab\", \"ax\"… 2. Select-able members must have more than two elements, with the first being the string of keys that lead to selecting it, and the second a short description string of the item. The command will then make a temporary buffer listing all entries that can be selected with a single key, and all the single key prefixes. When you press the key for a single-letter entry, it is selected. When you press a prefix key, the commands (and maybe further prefixes) under this key will be shown and offered for selection. TITLE will be placed over the selection in the temporary buffer, PROMPT will be used when prompting for a key. SPECIALS is an alist with (\"key\" \"description\") entries. When one of these is selected, only the bare key is returned." (save-window-excursion (let ((inhibit-quit t) (buffer (org-switch-to-buffer-other-window "*Org Select*")) (prompt (or prompt "Select: ")) case-fold-search current) (unwind-protect (catch 'exit (while t (setq-local evil-normal-state-cursor (list nil)) (erase-buffer) (insert title "\n\n") (let ((des-keys nil) (allowed-keys '("\C-g")) (tab-alternatives '("\s" "\t" "\r")) (cursor-type nil)) ;; Populate allowed keys and descriptions keys ;; available with CURRENT selector. (let ((re (format "\\`%s\\(.\\)\\'" (if current (regexp-quote current) ""))) (prefix (if current (concat current " ") ""))) (dolist (entry table) (pcase entry ;; Description. (`(,(and key (pred (string-match re))) ,desc) (let ((k (match-string 1 key))) (push k des-keys) ;; Keys ending in tab, space or RET are equivalent. (if (member k tab-alternatives) (push "\t" allowed-keys) (push k allowed-keys)) (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) (propertize "›" 'face 'font-lock-comment-face) " " desc "…" "\n"))) ;; Usable entry. (`(,(and key (pred (string-match re))) ,desc . ,_) (let ((k (match-string 1 key))) (insert (propertize prefix 'face 'font-lock-comment-face) (propertize k 'face 'bold) " " desc "\n") (push k allowed-keys))) (_ nil)))) ;; Insert special entries, if any. (when specials (insert "─────────────────────────\n") (pcase-dolist (`(,key ,description) specials) (insert (format "%s %s\n" (propertize key 'face '(bold all-the-icons-red)) description)) (push key allowed-keys))) ;; Display UI and let user select an entry or ;; a sub-level prefix. (goto-char (point-min)) (unless (pos-visible-in-window-p (point-max)) (org-fit-window-to-buffer)) (let ((pressed (org--mks-read-key allowed-keys prompt (not (pos-visible-in-window-p (1- (point-max))))))) (setq current (concat current pressed)) (cond ((equal pressed "\C-g") (user-error "Abort")) ;; Selection is a prefix: open a new menu. ((member pressed des-keys)) ;; Selection matches an association: return it. ((let ((entry (assoc current table))) (and entry (throw 'exit entry)))) ;; Selection matches a special entry: return the ;; selection prefix. ((assoc current specials) (throw 'exit current)) (t (error "No entry available"))))))) (when buffer (kill-buffer buffer)))))) (advice-add 'org-mks :override #'org-mks-pretty))
Templates
Set up the actual templates for different categories. Here’s a nice overview of
org-capture-template
variables.(after! org-capture (defun +doct-icon-declaration-to-icon (declaration) "Convert :icon declaration to icon" (let ((name (pop declaration)) (set (intern (concat "all-the-icons-" (plist-get declaration :set)))) (face (intern (concat "all-the-icons-" (plist-get declaration :color)))) (v-adjust (or (plist-get declaration :v-adjust) 0.01))) (apply set `(,name :face ,face :v-adjust ,v-adjust)))) (defun +doct-iconify-capture-templates (groups) "Add declaration's :icon to each template group in GROUPS." (let ((templates (doct-flatten-lists-in groups))) (setq doct-templates (mapcar (lambda (template) (when-let* ((props (nthcdr (if (= (length template) 4) 2 5) template)) (spec (plist-get (plist-get props :doct) :icon))) (setf (nth 1 template) (concat (+doct-icon-declaration-to-icon spec) "\t" (nth 1 template)))) template) templates)))) (setq doct-after-conversion-functions '(+doct-iconify-capture-templates)) (defvar +org-capture-recipies (concat (file-name-as-directory org-directory) "capture-recipies.org")) (defvar +org-capture-blog-file (concat (file-name-as-directory org-hugo-base-dir) "blog.org")) (defun set-org-capture-templates () ;; `org-roam' capture templates ;; Enabled by the `doct-org-roam' package installed from gist above. ;; FIXME - Can't make this work properly ;; (require 'doct-org-roam) ;; (setq org-roam-capture-templates ;; (doct-org-roam `(("Default" :keys "d" ;; :type plain ;; :file "%<%Y%m%d%H%M%S>-${slug}.org" ;; :head "#+title: ${title}\n\n" ;; :template "* Hallo" ;; :unnarrowed t) ;; ("Blog entry (Hugo)" :keys "b" ;; :headline "Hugo Blog" ;; :type entry ;; :file "%<%Y%m%d%H%M%S>-${slug}.org" ;; :template ("* %{title}" ":properties:" ;; ":export_file_name: %\\1" ;; ":export_description: %^{Description}" ;; ":export_date: %^{Date}t" ;; ":export_author: %n" ;; "🔚" ;; "" ;; "%?") ;; :custom (:title "%^{Title}") ;; :unnarrowed nil ;; )))) ;; `org' capture templates (setq org-capture-templates (doct `(("Todo" :keys "t" :icon ("checklist" :set "octicon" :color "green") :file +org-capture-todo-file :prepend t :headline "Inbox" :type entry :template ("* TODO %? %^G" "%i")) ("Note" :keys "n" :icon ("sticky-note-o" :set "faicon" :color "green") :file +org-capture-todo-file :prepend t :headline "Inbox" :type entry :template ("* %? %^G" "%i")) ;; ("Roam" :keys "r" ;; :icon ("pied-piper" :set "faicon" :color "pink") ;; :function org-roam-capture) ("Email" :keys "e" :icon ("envelope" :set "faicon" :color "blue") :file +org-capture-todo-file :prepend t :headline "Inbox" :type entry :template ("* TODO %^{type|reply to|contact} %\\3 %? ✉️" "Send an email %^{urgancy|soon|ASAP|anon|at some point|eventually} to %^{recipiant}" "about %^{topic}" "%U %i")) ("Interesting" :keys "i" :icon ("eye" :set "faicon" :color "lcyan") :file +org-capture-todo-file :prepend t :headline "Interesting" :type entry :template ("* [ ] %{desc}%? :%{i-type}:" "%i") :children (("Webpage" :keys "w" :icon ("globe" :set "faicon" :color "green") :desc "%(org-cliplink-capture) " :i-type "read:web") ("Article" :keys "a" :icon ("file-text" :set "octicon" :color "yellow") :desc "" :i-type "read:reaserch") ("\tRecipie" :keys "r" :icon ("spoon" :set "faicon" :color "dorange") :file +org-capture-recipies :headline "Unsorted" :template "%(org-chef-get-recipe-from-url)") ("Information" :keys "i" :icon ("info-circle" :set "faicon" :color "blue") :desc "" :i-type "read:info") ("Idea" :keys "I" :icon ("bubble_chart" :set "material" :color "purple") :desc "" :i-type "idea"))) ("Tasks" :keys "k" :icon ("inbox" :set "octicon" :color "yellow") :file +org-capture-todo-file :prepend t :headline "Tasks" :type entry :template ("* TODO %? %^G%{extra}" "%i") :children (("General Task" :keys "k" :icon ("inbox" :set "octicon" :color "green") :extra "") ("Capture point Task" :keys "c" :icon ("pencil" :set "octicon" :color "yellow") :extra "") ("Task with deadline" :keys "d" :icon ("timer" :set "material" :color "orange" :v-adjust -0.1) :extra "\nDEADLINE: %^{Deadline:}t") ("Scheduled Task" :keys "s" :icon ("calendar" :set "octicon" :color "orange") :extra "\nSCHEDULED: %^{Start time:}t"))) ("Project" :keys "p" :icon ("repo" :set "octicon" :color "purple") :prepend t :type entry :template ("* %{time-or-todo} %?" "%i") :file "" :custom (:time-or-todo "") :children (("Project-local todo" :keys "t" :icon ("checklist" :set "octicon" :color "green") :time-or-todo "TODO" :headline "TODO" :file +org-capture-project-todo-file) ("Project-local capture point task" :keys "c" :icon ("pencil" :set "octicon" :color "yellow") :template ("* TODO %?" "%a") :headline "TODO" :file +org-capture-project-todo-file) ("Project-local note" :keys "n" :icon ("sticky-note" :set "faicon" :color "yellow") :time-or-todo "%U" :headline "Notes" :file +org-capture-project-todo-file) ("Project-local changelog" :keys "c" :icon ("list" :set "faicon" :color "blue") :time-or-todo "%t" :headline "Changelog" :heading "Unreleased" :file +org-capture-project-changelog-file))) ("Blog" :keys "b" :icon ("pied-piper" :set "faicon" :color "pink") :file +org-capture-blog-file :prepend t :type entry :template ("* %{title} %^G" ":properties:" ":export_file_name: %\\1" ":export_description: %^{Description}" ":export_date: %^{Date}t" ":export_lastmod: %t" ":export_author: %n" "🔚" "\n%?") :custom (:title "%^{Title}")) ("Centralised project templates" :keys "o" :type entry :prepend t :template ("* %{time-or-todo} %?" "%i" "%a") :children (("Project todo" :keys "t" :prepend nil :time-or-todo "TODO" :heading "Tasks" :file +org-capture-projects-file) ("Project note" :keys "n" :time-or-todo "%U" :heading "Notes" :file +org-capture-notes-file) ("Project changelog" :keys "c" :time-or-todo "%U" :heading "Unreleased" :file +org-capture-changelog-file))) )))) (set-org-capture-templates) (unless (display-graphic-p) (add-hook 'server-after-make-frame-hook (defun org-capture-reinitialise-hook () (when (display-graphic-p) (set-org-capture-templates) (remove-hook 'server-after-make-frame-hook #'org-capture-reinitialise-hook))))))
It would also be nice to improve how the capture dialogue looks The org-capture bin is rather nice, but I’d be nicer with a smaller frame, and no modeline.
;; (setf (alist-get 'height +org-capture-frame-parameters) 15) ;; ;; (alist-get 'name +org-capture-frame-parameters) "❖ Capture") ;; ATM hardcoded in other places, so changing breaks stuff ;; (setq +org-capture-fn ;; (lambda () ;; (interactive) ;; (set-window-parameter nil 'mode-line-format 'none) ;; (org-capture)))
Web/Javascript/Typescript
Formatting
Prettier is an opinionated code formatter for many languages, most notably various Javascript dialects, HTML, CSS and friends.
(package! prettier)
(use-package! prettier
:hook (web-mode . prettier-mode))
Typescript
Remap typescript-mode
to the tree-sitter equivalent.
(add-list-to-list 'major-mode-remap-alist '((typescript-mode . typescript-ts-mode)
(typescript-tsx-mode . typescript-ts-mode)))
Hook up some extra tooling…
(use-package! typescript-ts-mode
:hook (typescript-ts-mode . prettier-mode)
:config
(add-hook! '(typescript-ts-mode-hook tsx-ts-mode-hook) #'lsp!))
… and optionally disable the old typescript-mode
.
;; (package! typescript-mode :disable t)
Python
LSP Python
Add some extra ignored-directories
for LSP.
(after! (python lsp-mode)
(add-to-list 'lsp-file-watch-ignored-directories "[/\\\\]\\.eggs\\'")
(add-to-list 'lsp-file-watch-ignored-directories "[/\\\\]\\build\\'")
;; Make sure pyright can find the virtualenv
;; (add-hook 'pyvenv-post-activate-hooks (lambda ()
;; (when (modulep! :lang python +pyright)
;; (setq lsp-pyright-venv-path pyvenv-virtual-env))))
)
Also, check out this handy guide for extra performance tips!
It’s recommended to use plist
for deserialization. In order to achieve that we need to export a
LSP_USE_PLISTS=true
environmental variable and ensure that Emacs knows about it (before lsp-mode
is compiled).
Debugging
DAP expects ptvsd by default as the Python debugger, however debugpy is recommended.
So be sure to install debugpy
.
$ pip3 install debugpy --user
(after! (python dap-mode)
(setq dap-python-debugger 'debugpy))
Elixir
Use the latest and greatest!
(unpin! (:lang elixir))
(unpin! elixir-mode)
(package! heex-ts-mode)
Check out configs linked here wkirschbaum/elixir-ts-mode#26 tree-sitter/:config: No language registered for…
(use-package! heex-ts-mode
:mode ("\\.heex\\'" . heex-ts-mode))
LSP Elixir
CredoLS
Credo is a static analysis tool for Elixir which provides great value and should be used in every project.
;; Override the `lsp-credo-version' variable to get the latest version. ;; It has to be set before `lsp-credo.el' is loaded. (custom-set-variables '(lsp-credo-version "0.3.0"))
Dialyxir
A flycheck
checker for dialixyr.
(package! flycheck-dialyxir)
(use-package! flycheck-dialyxir
:when (and (modulep! :checkers syntax)
(not (modulep! :checkers syntax +flymake)))
:after elixir-mode
:config (flycheck-dialyxir-setup))
Debugging
Debugging templates can be edited by invoking M-x dap-debug-edit-template
.
TODO - Figure out how to attach debugger to a running IEX session.
To configure Phoenix debugging
- elixir-lsp/elixir-ls#451 Debugger fails initialization on demo phoenix project
- Remember to set cowboy timeout or else it will timeout
- Env variables can be loaded using
load-env-vars
and loading either a.env
or.environment
file
(after! (elixir-mode lsp-elixir)
;; (require 'dap-elixir)
;; (defun dap-elixir--populate-start-file-args (conf)
;; "Populate CONF with the required arguments."
;; (-> conf
;; (dap--put-if-absent :dap-server-path '("debugger.sh"))
;; (dap--put-if-absent :type "mix_task")
;; (dap--put-if-absent :name "mix test")
;; (dap--put-if-absent :request "launch")
;; ;; (dap--put-if-absent :task "test")
;; ;; (dap--put-if-absent :taskArgs (list "--trace"))
;; (dap--put-if-absent :projectDir (lsp-find-session-folder (lsp-session) (buffer-file-name)))
;; (dap--put-if-absent :cwd (lsp-find-session-folder (lsp-session) (buffer-file-name)))
;; ;; (dap--put-if-absent :requireFiles (list
;; ;; "test/**/test_helper.exs"
;; ;; "test/**/*_test.exs"))
;; ))
(dap-register-debug-template "Elixir Run Configuration"
(list :type "Elixir"
:name "Elixir::Run"
:request "launch"
:task "test"
;; :command "iex"
:taskArgs (list "--trace")
:requiredFiles (list
"test/**/test_helper.exs"
"test/**/*_test.exs")
:dap-server-path (list (concat (file-name-as-directory lsp-elixir-ls-server-dir) "debugger.sh"))))
(dap-register-debug-template "Elixir Phoenix Server"
(list :type "Elixir"
:name "Elixir::Phoenix Server"
:request "launch"
:task "phx.server"
:dap-server-path (list (concat (file-name-as-directory lsp-elixir-ls-server-dir) "debugger.sh"))))
(dap-register-debug-template "Elixir Test Suite"
(list :name "Elixir::Test Suite"
:type "Elixir"
:request "launch"
:task "test"
:taskArgs (list "--trace")
:requiredFiles (list
"test/**/test_helper.exs"
"test/**/*_test.exs")
:dap-server-path (list (concat (file-name-as-directory lsp-elixir-ls-server-dir) "debugger.sh"))))
)
Dotnet
C-sharp
C-sharp is supported by default in newer versions of Emacs through csharp-tree-sitter-mode
.
Install the dotnet
package as well to get some extra goodies.
;; (package! dotnet)
LSP Dotnet
;; (add-hook! 'csharp-mode #'lsp) ;; (add-hook! 'csharp-tree-sitter-mode-hook #'lsp)
;; (use-package! dotnet ;; :hook ((csharp-mode . dotnet-mode) ;; (csharp-tree-sitter-mode . dotnet-mode))) ;; (add-to-list 'auto-mode-alist ;; '("\\.csproj\\'" . (lambda () (csproj-mode))))
DAP
To make DAP work nicely we need to install the netcoredbg. It’s supposed to be able to install automatically using
M-x dap-netcore-update-debugger
, but it’s not working correctly for me. The correct version can be downloaded from https://github.com/Samsung/netcoredbg/releases, and put in~/.config/emacs/.local/cache/.cache/lsp/netcoredbg
.(after! (dotnet dap-mode) (require 'dap-netcore) (setq dap-netcore-install-dir (f-join user-emacs-directory ".cache" "lsp")))
Keybindings
Add the
sharper-main-transient
menu to local leader.;; (map! :map csharp-tree-sitter-mode-map ;; :after csharp-tree-sitter ;; :localleader ;; :desc "Sharper" "s" #'sharper-main-transient)
Rust
Here is a good guide for setting up Emacs for Rust development.
(after! rustic
(setq rustic-lsp-server 'rust-analyzer
rustic-format-on-save t))
Rebind rustic
shortcuts to the localleader
menu.
Rustic
is an extension of rust-mode
which adds a number of useful features. Most rustic
features are bound to the C-c C-c
prefix, which I finds a bit cumbersome. Let’s rebind them to the localleader
so we have them easy accessible.
(map! :map rustic-mode-map
:after rustic
:localleader
(:prefix ("r" . "rustic")
:desc "recompile" "TAB" #'rustic-recompile
:desc "cargo-add" "a" #'rustic-cargo-add
:desc "cargo-bench" "b" #'rustic-cargo-bench
:desc "cargo-clean" "c" #'rustic-cargo-clean
:desc "cargo-doc" "d" #'rustic-cargo-doc
:desc "cargo-clippy-fix" "f" #'rustic-cargo-clippy-fix
:desc "cargo-init" "i" #'rustic-cargo-init
:desc "cargo-clippy" "k" #'rustic-cargo-clippy
:desc "cargo-new" "n" #'rustic-cargo-new
:desc "cargo-rm" "r" #'rustic-cargo-rm
:desc "cargo-upgrade" "u" #'rustic-cargo-upgrade
:desc "docstring-dwim" "C-," #'rustic-cargo-docstring-dwim
:desc "cargo-build" "C-b" #'rustic-cargo-build
:desc "cargo-current-test" "C-c" #'rustic-cargo-current-test
:desc "racer-describe" "C-d" #'rustic-racer-describe
:desc "cargo-fmt" "C-f" #'rustic-cargo-upgrade
:desc "cargo-check" "C-k" #'rustic-cargo-check
:desc "cargo-clippy" "C-l" #'rustic-cargo-clippy
:desc "cargo-outdated" "C-n" #'rustic-cargo-outdated
:desc "format-buffer" "C-o" #'rustic-format-buffer
:desc "cargo-run" "C-r" #'rustic-cargo-run
:desc "cargo-test" "C-t" #'rustic-cargo-test
:desc "compile" "C-u" #'rustic-compile)
;; lsp-rust-analyzer keybindings on localleader
(:when (eq rustic-lsp-server 'rust-analyzer)
:desc "Open Cargo.toml file" "c" #'lsp-rust-analyzer-open-cargo-toml
:desc "Open external docs" "D" #'lsp-rust-analyzer-open-external-docs))
Advice the *cargo-run*
buffer to accept user input (why doesn’t it already?).
(after! rustic
(defadvice! rustic-cargo-run-accept-user-input ()
"Advices the *cargo-run* buffer to run in comint-mode and accept user input"
:after 'rustic-cargo-run
(interactive)
(let ((current-window (selected-window))
(cargo-window (display-buffer (get-buffer "*cargo-run*") nil 'visible)))
(select-window cargo-window)
(comint-mode)
(read-only-mode 0)
(select-window current-window))))
LSP Rust
Configures lsp-mode
hints for Rust (using rust-analyzer
)
(From rust-emacs-setup guide)
(after! (rustic lsp-mode)
(when (eq rustic-lsp-server 'rust-analyzer)
(setq lsp-eldoc-render-all t
lsp-rust-analyzer-cargo-watch-command "clippy"
lsp-rust-analyzer-display-chaining-hints t
lsp-rust-analyzer-display-closure-return-type-hints t
lsp-rust-analyzer-display-lifetime-elision-hints-enable "skip_trivial"
lsp-rust-analyzer-display-lifetime-elision-hints-use-parameter-names nil
lsp-rust-analyzer-display-parameter-hints nil
lsp-rust-analyzer-display-reborrow-hints nil
lsp-rust-analyzer-server-display-inlay-hints t)))
Configures lsp-ui
for Rust
(after! (rustic lsp-ui-mode)
(setq lsp-ui-peek-always-show t
lsp-ui-sideline-show-hover t
lsp-ui-doc-enable nil))
Debugging
Configures dap-mode
for Rust.
We need to do some additional work for setting up debugging. (From rust-emacs-setup guide)
- Install
llvm
andcmake
via homebrew - Checkout the lldb-mi repo
- Build the
lldb-mi
binary - Link to a location in
$PATH
$ brew install cmake llvm
$ export LLVM_DIR=/usr/local/Cellar/llvm/14.0.6/lib/cmake
$ git clone https://github.com/lldb-tools/lldb-mi ~/.local/src/lldb-mi
$ mkdir -p ~/.local/src/lldb-mi/build
$ cd ~/.local/src/lldb-mi/build
$ cmake ..
$ cmake --build .
$ ln -s $PWD/src/lldb-mi ~/.local/bin/lldb-mi
TODO - Should set LLVM_DIR a better way?
(after! (rustic dap-mode)
(require 'dap-lldb)
(require 'dap-gdb-lldb)
(dap-gdb-lldb-setup)
(dap-register-debug-template "Rust::LLDB Run Configuration"
(list :type "lldb"
:request "launch"
:name "LLDB::Run"
:gdbpath "rust-lldb"
:target nil
:cwd nil)))
(dap-gdb-lldb-setup) will install a VS Code extension into user-emacs-dir/.extension/vscode/webfreak.debug. One problem I observed was that this installation is not always successful. Should you end up without a “webfreak.debug” directory you might need to delete the vscode/ folder and run (dap-gdb-lldb-setup) again.
EDIT - It turns out my “webfreak.debug” directory is empty - Figure this out at some time!
Finally run sudo DevToolsSecurity --enable
to allow the debugger access to processes.
$ sudo DevToolsSecurity --enable
Enter PIN for 'Certificate For PIV Authentication (Yubico PIV Authentication)':
Developer mode is now enabled.
Golang
Installing Go
I’m using ASDF for managing various programming languages.
$ asdf install golang latest
$ asdf global golang latest
Next, there’s a bunch of dependencies we need to install in order to have a smooth experience.
Make sure the
GOPATH
environment variable is properly set!
gocode
for code completion andeldoc
supportgodoc
for documentation lookupgorename
for extra refactoring commandsguru
for code navigation and refactoring commandsgore
REPLgoimports
optional auto-formatting on saving files and fixing importsgotest
for generating test codegomodifytags
for manipulating tagsgopls
language servergotestsum
a friendly test runnergovulncheck
finds known vulnerabilities in project dependencies
$ go install github.com/x-motemen/gore/cmd/gore@latest
$ go install github.com/stamblerre/gocode@latest
$ go install golang.org/x/tools/cmd/godoc@latest
$ go install golang.org/x/tools/cmd/goimports@latest
$ go install golang.org/x/tools/cmd/gorename@latest
$ go install golang.org/x/tools/cmd/guru@latest
$ go install github.com/cweill/gotests/gotests@latest
$ go install github.com/fatih/gomodifytags@latest
$ go install golang.org/x/tools/gopls@latest
$ go install gotest.tools/gotestsum@latest
$ go install golang.org/x/vuln/cmd/govulncheck@latest
Security checker
The gosec
tool inspects source code for security problems by scanning the Go AST.
$ curl -sfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
Linting
Finally, install golangci-lint
for flycheck
integration. It is recommended to install pre-built binaries.
$ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2
Configuration
When using the new go-ts-mode
instead of go-mode
, we loose some functionality that probably will
be fixed in upstreams Doom config soon.
(defun lsp-go-install-save-hooks ()
"Set up some before-save hooks to format buffer and add/delete imports"
(add-hook! 'before-save-hook #'lsp-format-buffer t t)
(add-hook! 'before-save-hook #'lsp-organize-imports t t))
Add the go-ts-mode
to major-mode-remap-alist
so that we use the tree-sitter mode when coding Go.
(add-to-list 'major-mode-remap-alist '(go-mode . go-ts-mode))
;; (use-package! go-ts-mode
;; :config
;; (add-hook! 'go-ts-mode-hook #'lsp!)
;; (add-hook! 'go-ts-mode-hook #'lsp-go-install-save-hooks))
;; (use-package emacs
;; :ensure nil
;; :config
;; (setq major-mode-remap-alist
;; '((go-mode . go-ts-mode))))
(after! (go flycheck lsp-ui)
(flycheck-golangci-lint-setup))