Daw-Ran Liou’s Emacs Configuration
This is an ongoing evolution of my original Emacs configuration files, inspired by a bunch of resources I’ve found online:
- https://github.com/hlissner/doom-emacs
- https://github.com/daviwil/dotfiles/blob/master/Emacs.org
- https://github.com/daviwil/emacs-from-scratch/blob/master/Emacs.org
- https://github.com/meqif/.emacs.d
- https://github.com/raxod502/radian/blob/develop/emacs/radian.el
- https://github.com/oantolin/emacs-config
- https://github.com/mpereira/.emacs.d
Style guide:
A recent screenshot:
Table of Contents
- Table of Contents
- Early-init.el
- Startup Performance
- Private Lisp
- Keep .emacs.d Clean
- Package System Setup
- MacOS
- Keybindings
- UI
- Blackout Mode Line Lighters
- Keybinding Panel (which-key)
- Clean up Emacs’ UI to be more minimal
- Scratch Buffer
- Display line number
- Highlight line (disabled)
- Highlight on Idle
- Themes
- Font configuration
- Modeline
- Highlight Matching Parens
- Paren Face
- Window Management
- Highlight Fill Column
- Center Buffers
- Emoji and Unicode
- Native Titlebar
- Rainbow Mode
- Hide Mode Line
- Completion
- Helpful Help Commands
- Buffers and Files
- Editing
- Org Mode
- Dired
- Shell
- Development
- Extras
- App
Early-init.el
From The Early Init File:
Most customizations for Emacs should be put in the normal init file. See Init File. However, it is sometimes desirable to have customizations that take effect during Emacs startup earlier than the normal init file is processed. Such customizations can be put in the early init file, ~/.config/emacs/early-init.el or ~/.emacs.d/early-init.el. This file is loaded before the package system and GUI is initialized, so in it you can customize variables that affect frame appearance as well as the package initialization process, such as package-enable-at-startup, package-load-list, and package-user-dir.
;;; early-init.el -*- lexical-binding: t; -*-
;; NOTE: early-init.el is now generated from Emacs.org. Please edit that file
;; in Emacs and early-init.el will be generated automatically!
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6)
(setq package-enable-at-startup nil)
(push '(menu-bar-lines . 0) default-frame-alist)
(push '(tool-bar-lines . 0) default-frame-alist)
(push '(vertical-scroll-bars . nil) default-frame-alist)
(setq frame-inhibit-implied-resize t)
Startup Performance
Source: How does Doom start up so quickly?
;; -*- lexical-binding: t; -*-
;; NOTE: init.el is now generated from Emacs.org. Please edit that file in
;; Emacs and init.el will be generated automatically!
GC
The GC can easily double startup time, so we suppress it at startup by turning
up gc-cons-threshold
(and perhaps gc-cons-percentage
) temporarily.
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6)
However, it is important to reset it eventually. Not doing so will cause garbage
collection freezes during long-term interactive use. Conversely, a
gc-cons-threshold
that is too small will cause stuttering. We use 16mb as our
default.
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold 16777216 ; 16mb
gc-cons-percentage 0.1)))
It may also be wise to raise gc-cons-threshold
while the minibuffer is active,
so the GC doesn’t slow down expensive commands (or completion frameworks, like
helm and ivy). Here is how Doom does it:
(defun doom-defer-garbage-collection-h ()
(setq gc-cons-threshold most-positive-fixnum))
(defun doom-restore-garbage-collection-h ()
;; Defer it so that commands launched immediately after will enjoy the
;; benefits.
(run-at-time
1 nil (lambda () (setq gc-cons-threshold 16777216)))) ; 16mb
(add-hook 'minibuffer-setup-hook #'doom-defer-garbage-collection-h)
(add-hook 'minibuffer-exit-hook #'doom-restore-garbage-collection-h)
file-name-handler-alist
temporarily
Unset Emacs consults this variable every time a file is read or library loaded, or
when certain functions in the file API are used (like expand-file-name
or
file-truename
).
Emacs does this to check if a special handler is needed to read that file, but none of them are (typically) necessary at startup, so we disable them (temporarily!):
(defvar doom--file-name-handler-alist file-name-handler-alist)
(setq file-name-handler-alist nil)
;; Alternatively, restore it even later:
(add-hook 'emacs-startup-hook
(lambda ()
(setq file-name-handler-alist doom--file-name-handler-alist)))
Measure the startup time
;; Profile emacs startup
(add-hook 'emacs-startup-hook
(lambda ()
(message "*** Emacs loaded in %s with %d garbage collections."
(format "%.2f seconds"
(float-time
(time-subtract after-init-time before-init-time)))
gcs-done)))
Private Lisp
Load private.el
after init.
(add-hook
'after-init-hook
(lambda ()
(let ((private-file (concat user-emacs-directory "private.el")))
(when (file-exists-p private-file)
(load-file private-file)))))
Keep .emacs.d Clean
Put backups and auto-save files in their own folders.
;; Keep backup files and auto-save files in the backups directory
(setq backup-directory-alist
`(("." . ,(expand-file-name "backups" user-emacs-directory)))
auto-save-file-name-transforms
`((".*" ,(expand-file-name "auto-save-list/" user-emacs-directory) t)))
Put custom settings into its own file.
(setq custom-file (concat user-emacs-directory "custom.el"))
(load custom-file 'noerror)
Package System Setup
straight.el for reproducible package management.
(setq straight-use-package-by-default t
straight-build-dir (format "build-%s" emacs-version))
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
Emacs has a built in package manager but it doesn’t make it easy to automatically install packages on a new system the first time you pull down your configuration. use-package is a really helpful package used in this configuration to make it a lot easier to automate the installation and configuration of everything else we use.
(straight-use-package 'use-package)
(setq use-package-verbose t)
MacOS
(mac-auto-operator-composition-mode)
(setq-default delete-by-moving-to-trash t)
;; Both command keys are 'Super'
(setq mac-right-command-modifier 'super)
(setq mac-command-modifier 'super)
;; Option or Alt is naturally 'Meta'
(setq mac-option-modifier 'meta)
(setq mac-right-option-modifier 'meta)
;; Make keybindings feel natural on mac
(global-set-key (kbd "s-s") 'save-buffer) ;; save
(global-set-key (kbd "s-S") 'write-file) ;; save as
(global-set-key (kbd "s-q") 'save-buffers-kill-emacs) ;; quit
(global-set-key (kbd "s-a") 'mark-whole-buffer) ;; select all
(global-set-key (kbd "s-k") 'kill-this-buffer)
(global-set-key (kbd "s-v") 'yank)
(global-set-key (kbd "s-c") 'kill-ring-save)
(global-set-key (kbd "s-z") 'undo)
(global-set-key (kbd "s-=") 'text-scale-adjust)
(global-set-key (kbd "s-+") 'text-scale-increase)
Keybindings
This configuration uses evil-mode for a Vi-like modal editing experience. general.el is used for easy keybinding configuration that integrates well with which-key. evil-collection is used to automatically configure various Emacs modes with Vi-like keybindings for evil-mode.
ESC Cancels All
;; Make ESC quit prompts
(global-set-key (kbd "<escape>") 'keyboard-escape-quit)
Rebind C-u
Since I let evil-mode
take over C-u
for buffer scrolling, I need to re-bind
the universal-argument
command to another key sequence. I’m choosing C-M-u
for this purpose.
(global-set-key (kbd "C-M-u") 'universal-argument)
Evil
Some tips can be found here:
- https://github.com/noctuid/evil-guide
- https://nathantypanski.com/blog/2014-08-03-a-vim-like-emacs-config.html
(use-package evil
:init
(setq evil-want-integration t)
(setq evil-want-keybinding nil)
(setq evil-want-C-u-scroll t)
(setq evil-want-C-i-jump t)
(setq evil-move-beyond-eol t)
(setq evil-move-cursor-back nil)
:custom
(evil-undo-system 'undo-fu)
(evil-symbol-word-search t)
:config
(evil-mode 1)
(define-key evil-insert-state-map (kbd "C-g") 'evil-normal-state)
(define-key evil-normal-state-map "\C-e" 'evil-end-of-line)
(define-key evil-insert-state-map "\C-e" 'end-of-line)
(define-key evil-visual-state-map "\C-e" 'evil-end-of-line)
(define-key evil-motion-state-map "\C-e" 'evil-end-of-line)
(define-key evil-normal-state-map "\C-y" 'yank)
(define-key evil-insert-state-map "\C-y" 'yank)
(define-key evil-visual-state-map "\C-y" 'yank)
(define-key evil-normal-state-map "\C-k" 'kill-line)
(define-key evil-insert-state-map "\C-k" 'kill-line)
(define-key evil-visual-state-map "\C-k" 'kill-line)
;; Get around faster
(define-key evil-motion-state-map "gs" 'evil-avy-goto-symbol-1)
(define-key evil-motion-state-map "gS" 'evil-avy-goto-char-timer)
;; Use visual line motions even outside of visual-line-mode buffers
(evil-global-set-key 'motion "j" 'evil-next-visual-line)
(evil-global-set-key 'motion "k" 'evil-previous-visual-line)
(evil-set-initial-state 'messages-buffer-mode 'normal)
(evil-set-initial-state 'dashboard-mode 'normal)
;; Let emacs bindings for M-. and M-, take over
(define-key evil-normal-state-map (kbd "M-.") nil)
(define-key evil-normal-state-map (kbd "M-,") nil)
(global-set-key (kbd "s-w") 'evil-window-delete))
(use-package evil-collection
:config
(evil-collection-init))
;; Allows you to use the selection for * and #
(use-package evil-visualstar
:commands (evil-visualstar/begin-search
evil-visualstar/begin-search-forward
evil-visualstar/begin-search-backward)
:init
(evil-define-key 'visual 'global
"*" #'evil-visualstar/begin-search-forward
"#" #'evil-visualstar/begin-search-backward))
Simplify Leader Bindings (general.el)
(use-package general
:config
(general-create-definer dawran/leader-keys
:states '(normal insert visual emacs)
:keymaps 'override
:prefix "SPC"
:global-prefix "M-SPC")
(general-create-definer dawran/localleader-keys
:states '(normal insert visual emacs)
:keymaps 'override
:major-modes t
:prefix ","
:non-normal-prefix "C-,")
(dawran/leader-keys
"fd" '((lambda () (interactive) (find-file (expand-file-name "~/.emacs.d/README.org"))) :which-key "edit config")
"t" '(:ignore t :which-key "toggles")
"tt" '(dawran/load-theme :which-key "choose theme")
"tw" 'whitespace-mode
"tm" 'toggle-frame-maximized
"tM" 'toggle-frame-fullscreen))
Better Default Bindings
(global-set-key (kbd "C-x C-b") #'ibuffer)
(global-set-key (kbd "C-M-j") #'switch-to-buffer)
(global-set-key (kbd "M-:") 'pp-eval-expression)
UI
Blackout Mode Line Lighters
Blackout is an easy way to turn off mode line lighters. It’s similar to diminish.el or delight.el. See the comparisons at: https://github.com/raxod502/blackout.
(use-package blackout
:straight (:host github :repo "raxod502/blackout"))
(use-package autorevert
:defer t
:blackout auto-revert-mode)
Keybinding Panel (which-key)
which-key is a useful UI panel that appears when you start pressing any key
binding in Emacs to offer you all possible completions for the prefix. For
example, if you press C-c
(hold control and press the letter c
), a panel
will appear at the bottom of the frame displaying all of the bindings under that
prefix and which command they run. This is very useful for learning the
possible key bindings in the mode of your current buffer.
(use-package which-key
:blackout t
:hook (after-init . which-key-mode)
:diminish which-key-mode
:config
(setq which-key-idle-delay 1))
Clean up Emacs’ UI to be more minimal
(setq inhibit-startup-message t)
(setq frame-inhibit-implied-resize t)
(setq default-frame-alist
(append (list
'(font . "Monolisa-14")
'(min-height . 1) '(height . 45)
'(min-width . 1) '(width . 81)
)))
;; No beeping nor visible bell
(setq ring-bell-function #'ignore
visible-bell nil)
(blink-cursor-mode 0)
(setq-default fill-column 80)
(setq-default line-spacing 1)
Scratch Buffer
(defvar scratch-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c c") 'lisp-interaction-mode)
(define-key map (kbd "C-c C-c") 'lisp-interaction-mode)
map)
"Keymap for `scratch-mode'.")
(define-derived-mode scratch-mode
fundamental-mode
"Scratch"
"Major mode for the *scratch* buffer.\\{scratch-mode-map}"
(setq-local indent-line-function 'indent-relative))
(setq initial-major-mode 'scratch-mode)
(setq initial-scratch-message nil)
(defun jump-to-scratch-buffer ()
"Jump to the existing *scratch* buffer or create a new one."
(interactive)
(let ((scratch-buffer (get-buffer-create "*scratch*")))
(unless (derived-mode-p 'scratch-mode)
(with-current-buffer scratch-buffer
(scratch-mode)))
(switch-to-buffer scratch-buffer)))
(global-set-key (kbd "s-t") #'jump-to-scratch-buffer)
Display line number
(column-number-mode)
;; Enable line numbers for prog modes only
(add-hook 'prog-mode-hook (lambda () (display-line-numbers-mode 1)))
Highlight line (disabled)
(use-package hl-line
:disabled t
:hook
(prog-mode . hl-line-mode))
Highlight on Idle
(use-package idle-highlight-mode
:blackout t
:hook
(prog-mode . idle-highlight-mode))
Themes
I’m using my personal theme - oil6
as my prefered theme.
(add-to-list 'custom-theme-load-path "~/.emacs.d/themes")
Here’s my other published themes
(use-package sketch-themes
:straight (:host github :repo "dawranliou/sketch-themes"))
Load Theme Action
Loading themes on top of one another usually have unwanted side effects of residual faces from the previous ones. I like to keep multiple themes at disposal at the same time. Each one of them have different emphasis and philosophy behind. Rather than making sure the themes overrides the leftover faces properly, the simpler way to address this is by disabling all other enabled themes.
This is inspired by abo-abo’s counsel-load-theme-action.
(defvar dawran/after-load-theme-hook nil
"Hook run after a color theme is loaded using `load-theme'.")
(defun dawran/load-theme-action (theme)
"Disable current themes and load theme THEME."
(condition-case nil
(progn
(mapc #'disable-theme custom-enabled-themes)
(load-theme (intern theme) t)
(run-hooks 'dawran/after-load-theme-hook))
(error "Problem loading theme %s" theme)))
(defun dawran/load-theme ()
"Disable current themes and load theme from the completion list."
(interactive)
(let ((theme (completing-read "Load custom theme: "
(mapcar 'symbol-name
(custom-available-themes)))))
(dawran/load-theme-action theme)))
(dawran/load-theme-action "sketch-white")
Font configuration
;; Set the fixed pitch face
(set-face-attribute 'fixed-pitch nil :font "Monolisa" :height 140)
;; Set the variable pitch face
(set-face-attribute 'variable-pitch nil :font "Cantarell" :height 170)
Modeline
The simple mode line is mostly stolen from: https://github.com/raxod502/radian/blob/develop/emacs/radian.el
;;;; Mode line
;; The following code customizes the mode line to something like:
;; [*] radian.el 18% (18,0) [radian:develop*] (Emacs-Lisp)
(defun my/mode-line-buffer-modified-status ()
"Return a mode line construct indicating buffer modification status.
This is [*] if the buffer has been modified and whitespace
otherwise. (Non-file-visiting buffers are never considered to be
modified.) It is shown in the same color as the buffer name, i.e.
`mode-line-buffer-id'."
(propertize
(if (and (buffer-modified-p)
(buffer-file-name))
"[*]"
" ")
'face 'mode-line-buffer-id))
;; Normally the buffer name is right-padded with whitespace until it
;; is at least 12 characters. This is a waste of space, so we
;; eliminate the padding here. Check the docstrings for more
;; information.
(setq-default mode-line-buffer-identification
(propertized-buffer-identification "%b"))
;; Make `mode-line-position' show the column, not just the row.
(column-number-mode +1)
;; https://emacs.stackexchange.com/a/7542/12534
(defun my/mode-line-align (left right)
"Render a left/right aligned string for the mode line.
LEFT and RIGHT are strings, and the return value is a string that
displays them left- and right-aligned respectively, separated by
spaces."
(let ((width (- (window-total-width) (length left))))
(format (format "%%s%%%ds" width) left right)))
(defcustom my/mode-line-left
'(;; Show [*] if the buffer is modified.
(:eval (my/mode-line-buffer-modified-status))
" "
;; Show the name of the current buffer.
mode-line-buffer-identification
" "
;; Show the row and column of point.
mode-line-position
evil-mode-line-tag)
"Composite mode line construct to be shown left-aligned."
:type 'sexp)
(defcustom my/mode-line-right
'(""
mode-line-modes)
"Composite mode line construct to be shown right-aligned."
:type 'sexp)
;; Actually reset the mode line format to show all the things we just
;; defined.
(setq-default mode-line-format
'(:eval (replace-regexp-in-string
"%" "%%"
(my/mode-line-align
(format-mode-line my/mode-line-left)
(format-mode-line my/mode-line-right))
'fixedcase 'literal)))
Highlight Matching Parens
Display highlighting on whatever paren matches the one before or after point.
(use-package paren
:hook (prog-mode . show-paren-mode))
Implementing Show matching lines when parentheses go off-screen by Clemens Radermacher
(use-package paren-blink
:disabled t
:straight nil
:load-path "lisp/")
Paren Face
paren-face dims the parentheses to reduce visual distractions.
(use-package paren-face
:hook
(lispy-mode . paren-face-mode))
Window Management
(use-package ace-window
:bind (("M-o" . ace-window))
:config
(setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)))
(use-package winner-mode
:straight nil
:bind (:map evil-window-map
("u" . winner-undo)
("U" . winner-redo))
:general
(dawran/leader-keys
"w" 'evil-window-map)
:config
(winner-mode))
Highlight Fill Column
(use-package hl-fill-column
:hook (prog-mode . hl-fill-column-mode))
Center Buffers
(defun dawran/visual-fill ()
(setq visual-fill-column-width 100
visual-fill-column-center-text t)
(visual-fill-column-mode 1))
(use-package visual-fill-column
:commands visual-fill-column-mode)
Emoji and Unicode
(use-package unicode-fonts
:defer t
:config
(unicode-fonts-setup))
Native Titlebar
(use-package ns-auto-titlebar
:hook (after-init . ns-auto-titlebar-mode))
(setq ns-use-proxy-icon nil
frame-title-format nil)
Rainbow Mode
(use-package rainbow-mode
:commands rainbow-mode)
Hide Mode Line
(use-package hide-mode-line
:commands hide-mode-line-mode)
Completion
Selectrum
(setq enable-recursive-minibuffers t)
;; Package `selectrum' is an incremental completion and narrowing
;; framework. Like Ivy and Helm, which it improves on, Selectrum
;; provides a user interface for choosing from a list of options by
;; typing a query to narrow the list, and then selecting one of the
;; remaining candidates. This offers a significant improvement over
;; the default Emacs interface for candidate selection.
(use-package selectrum
:straight (:host github :repo "raxod502/selectrum")
:bind (("C-M-r" . selectrum-repeat)
:map selectrum-minibuffer-map
("C-r" . selectrum-select-from-history)
("C-j" . selectrum-next-candidate)
("C-k" . selectrum-previous-candidate))
:custom
(selectrum-count-style 'current/matches)
(selectrum-fix-minibuffer-height t)
:init
;; This doesn't actually load Selectrum.
(selectrum-mode +1))
;; Package `prescient' is a library for intelligent sorting and
;; filtering in various contexts.
(use-package prescient
:config
;; Remember usage statistics across Emacs sessions.
(prescient-persist-mode +1)
;; The default settings seem a little forgetful to me. Let's try
;; this out.
(setq prescient-history-length 1000))
;; Package `selectrum-prescient' provides intelligent sorting and
;; filtering for candidates in Selectrum menus.
(use-package selectrum-prescient
:straight (:host github :repo "raxod502/prescient.el"
:files ("selectrum-prescient.el"))
:after selectrum
:config
(selectrum-prescient-mode +1))
Marginalia
(use-package marginalia
:bind (:map minibuffer-local-map
("C-M-a" . marginalia-cycle))
:init
(marginalia-mode)
;; When using Selectrum, ensure that Selectrum is refreshed when cycling annotations.
(advice-add #'marginalia-cycle :after
(lambda () (when (bound-and-true-p selectrum-mode) (selectrum-exhibit))))
(setq marginalia-annotators '(marginalia-annotators-heavy
marginalia-annotators-light nil)))
CtrlF
;; Package `ctrlf' provides a replacement for `isearch' that is more
;; similar to the tried-and-true text search interfaces in web
;; browsers and other programs (think of what happens when you type
;; ctrl+F).
(use-package ctrlf
:straight (:host github :repo "raxod502/ctrlf")
:bind
("s-f" . ctrlf-forward-fuzzy)
:init
(ctrlf-mode +1)
:config
(defun ctrlf-toggle-fuzzy ()
"Toggle CTRLF style to `fuzzy' or back to `literal'."
(interactive)
(setq ctrlf--style
(if (eq ctrlf--style 'fuzzy) 'literal 'fuzzy)))
(add-to-list 'ctrlf-minibuffer-bindings
'("s-f" . ctrlf-toggle-fuzzy)))
Embark
(use-package embark
:bind
(("C-S-a" . embark-act)
:map minibuffer-local-map
("C-d" . embark-act))
:config
;; For Selectrum users:
(defun current-candidate+category ()
(when selectrum-active-p
(cons (selectrum--get-meta 'category)
(selectrum-get-current-candidate))))
(add-hook 'embark-target-finders #'current-candidate+category)
(defun current-candidates+category ()
(when selectrum-active-p
(cons (selectrum--get-meta 'category)
(selectrum-get-current-candidates
;; Pass relative file names for dired.
minibuffer-completing-file-name))))
(add-hook 'embark-candidate-collectors #'current-candidates+category)
;; No unnecessary computation delay after injection.
(add-hook 'embark-setup-hook 'selectrum-set-selected-candidate)
:custom
(embark-action-indicator
(lambda (map)
(which-key--show-keymap "Embark" map nil nil 'no-paging)
#'which-key--hide-popup-ignore-command)
embark-become-indicator embark-action-indicator))
Helpful Help Commands
Helpful adds a lot of very helpful (get it?) information to Emacs’ describe-
command buffers. For example, if you use describe-function
, you will not only
get the documentation about the function, you will also see the source code of
the function and where it gets used in other places in the Emacs configuration.
It is very useful for figuring out how things work in Emacs.
(use-package helpful
:bind (;; Remap standard commands.
([remap describe-function] . #'helpful-callable)
([remap describe-variable] . #'helpful-variable)
([remap describe-key] . #'helpful-key)
([remap describe-symbol] . #'helpful-symbol)
("C-c C-d" . #'helpful-at-point)
("C-h C" . #'helpful-command)
("C-h F" . #'describe-face)))
Buffers and Files
Persistent Scratch
(use-package persistent-scratch
:custom
(persistent-scratch-autosave-interval 60)
:config
(persistent-scratch-setup-default))
Recent Files
(use-package recentf
:defer 1
:custom
;; Increase recent entries list from default (20)
(recentf-max-saved-items 100)
:config
(recentf-mode +1))
Editing
UTF-8
(prefer-coding-system 'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
Tabs
Default to an indentation size of 2 spaces since it’s the norm for pretty much every language I use.
(setq-default tab-width 4)
(setq-default evil-shift-width tab-width)
(setq-default indent-tabs-mode nil)
Commenting Lines
(use-package evil-nerd-commenter
:bind ("s-/" . evilnc-comment-or-uncomment-lines))
Automatically Clean Whitespace
(use-package ws-butler
:blackout t
:hook ((text-mode . ws-butler-mode)
(prog-mode . ws-butler-mode))
:custom
;; ws-butler normally preserves whitespace in the buffer (but strips it from
;; the written file). While sometimes convenient, this behavior is not
;; intuitive. To the average user it looks like whitespace cleanup is failing,
;; which causes folks to redundantly install their own.
(ws-butler-keep-whitespace-before-point nil))
Lisp S-expression Editing
I prefer to use lispy and lispyville for lisp structural editing.
(use-package lispy
:blackout t
:hook ((emacs-lisp-mode . lispy-mode)
(clojure-mode . lispy-mode)
(clojurescript-mode . lispy-mode)
(cider-repl-mode . lispy-mode))
:custom
(lispy-close-quotes-at-end-p t))
(use-package lispyville
:blackout t
:hook ((lispy-mode . lispyville-mode))
:custom
(lispyville-key-theme '(operators
c-w
(prettify insert)
additional
additional-insert
additional-movement
additional-wrap
(atom-movement normal visual)
commentary
slurp/barf-cp))
:config
(lispy-set-key-theme '(lispy c-digits))
(lispyville-set-key-theme))
Evil Multiedit
I really like evil-multiedit to do multiple cursor edits.
(use-package evil-multiedit
:bind (:map evil-visual-state-map
("R" . evil-multiedit-match-all)
("M-d" . evil-multiedit-match-and-next)
("M-D" . evil-multiedit-match-and-prev)
("C-M-d" . evil-multiedit-restore)
:map evil-normal-state-map
("M-d" . evil-multiedit-match-symbol-and-next)
("M-D" . evil-multiedit-match-symbol-and-prev)
("C-M-d" . evil-multiedit-restore)
:map evil-insert-state-map
("M-d" . evil-multiedit-toggle-marker-here)
:map evil-motion-state-map
("RET" . evil-multiedit-toggle-or-restrict-region)
:map evil-multiedit-state-map
("RET" . evil-multiedit-toggle-or-restrict-region)
("C-n" . evil-multiedit-next)
("C-p" . evil-multiedit-prev)
:map evil-multiedit-insert-state-map
("C-n" . evil-multiedit-next)
("C-p" . evil-multiedit-prev)))
Undo-fu
(use-package undo-fu)
Electric Pair
Automatically close brackets, parens, etc. Bundled with Emacs.
(use-package elec-pair
:straight nil
:config
(electric-pair-mode 1))
Expand Region
(use-package expand-region
:bind
("s-'" . er/expand-region)
("s-\"" . er/contract-region)
:hook
(prog-mode . my/greedy-expansion-list)
:config
(defun my/greedy-expansion-list ()
"Skip marking words or inside quotes and pairs"
(setq-local er/try-expand-list
(cl-set-difference er/try-expand-list
'(er/mark-word
er/mark-inside-quotes
er/mark-inside-pairs)))))
Savehist
Remember history of things across launches (ie. kill ring).
(use-package savehist
:hook (after-init . savehist-mode)
:custom
(savehist-file "~/.emacs.d/savehist")
(savehist-save-minibuffer-history t)
(savehist-additional-variables
'(kill-ring
mark-ring global-mark-ring
search-ring regexp-search-ring))
(history-length 20000))
Saveplace
When you visit a file, point goes to the last place where it was when you previously visited the same file.
(use-package saveplace
:config
(save-place-mode t))
Org Mode
Basic Config
(defun dawran/org-mode-setup ()
;; hide title / author ... keywords
(setq-local org-hidden-keywords '(title author date))
(setq-local electric-pair-inhibit-predicate
`(lambda (c)
(if (char-equal c ?<)
t
(,electric-pair-inhibit-predicate c))))
;; Indentation
;; (org-indent-mode)
(blackout 'org-indent-mode)
;; (variable-pitch-mode 1)
(blackout 'buffer-face-mode)
(visual-line-mode 1)
(blackout 'visual-line-mode)
(dawran/visual-fill))
(use-package org
:hook (org-mode . dawran/org-mode-setup)
:custom
(org-hide-emphasis-markers t)
(org-src-fontify-natively t)
(org-src-tab-acts-natively t)
(org-src-window-setup 'current-window)
(org-cycle-separator-lines 1)
(org-edit-src-content-indentation 0)
(org-src-window-setup 'current-window)
(org-indirect-buffer-display 'current-window)
(org-hide-block-startup nil)
(org-src-preserve-indentation nil)
(org-adapt-indentation nil)
;; (org-startup-folded 'content)
(org-log-done 'time)
(org-log-into-drawer t)
(org-image-actual-width 640)
(org-attach-auto-tag "attachment"))
(use-package org-tempo
:straight nil
:after org
:config
(add-to-list 'org-structure-template-alist '("sh" . "src shell"))
(add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp")))
(use-package evil-org
:blackout t
:after evil
:hook (org-mode . evil-org-mode))
Auto-tangle Configuration Files
(defun dawran/org-babel-tangle-config ()
"Automatically tangle our Emacs.org config file when we save it."
(when (string-equal (buffer-file-name)
(expand-file-name "./README.org"))
;; Dynamic scoping to the rescue
(let ((org-confirm-babel-evaluate nil))
(org-babel-tangle))))
(add-hook 'org-mode-hook (lambda () (add-hook 'after-save-hook #'dawran/org-babel-tangle-config)))
Update Table of Contents on Save
(use-package org-make-toc
:hook (org-mode . org-make-toc-mode))
Journal
(use-package org-journal
:general
(dawran/leader-keys
"n" '(:ignore t :which-key "notes")
"nj" '(org-journal-open-current-journal-file :which-key "journal")
"nJ" '(org-journal-new-entry :which-key "new journal entry"))
:custom
(org-journal-date-format "%A, %d/%m/%Y")
(org-journal-date-prefix "* ")
(org-journal-file-format "%F.org")
(org-journal-dir "~/org/journal/")
(org-journal-file-type 'weekly)
(org-journal-find-file #'find-file))
Roam
(use-package org-roam
:custom
(org-roam-directory "~/org/roam/")
:general
(dawran/leader-keys
"nf" 'org-roam-find-file
:keymaps 'org-roam-mode-map
"nl" 'org-roam
"ng" 'org-roam-graph-show
:keymaps 'org-mode-map
"ni" 'org-roam-insert
"nI" 'org-roam-insert-immediate))
Presentation
(use-package org-tree-slide
:commands (org-tree-slide-mode)
:custom
(org-image-actual-width nil)
(org-tree-slide-slide-in-effect nil)
(org-tree-slide-activate-message "Presentation started.")
(org-tree-slide-deactivate-message "Presentation ended.")
(org-tree-slide-breadcrumbs " > ")
(org-tree-slide-header t))
Paste Clipboard Image into Org files
Inspired by mpereira’s config.
(defvar org-paste-clipboard-image-dir "img")
(defun dawran/org-paste-clipboard-image ()
"Paste clipboard image to org file."
(interactive)
(if (not (executable-find "pngpaste"))
(message "Requires pngpaste in PATH")
(unless (file-exists-p org-paste-clipboard-image-dir)
(make-directory org-paste-clipboard-image-dir t))
(let ((image-file (format "%s/%s.png"
org-paste-clipboard-image-dir
(make-temp-name "org-image-paste-"))))
(call-process-shell-command (format "pngpaste %s" image-file))
(insert (format "#+CAPTION: %s\n" (read-string "Caption: ")))
(insert (format "[[file:%s]]" image-file))
(org-display-inline-images))))
(with-eval-after-load "org"
(define-key org-mode-map (kbd "s-y") #'dawran/org-paste-clipboard-image))
Dired
(use-package dired
:straight nil
;; :hook (dired-mode . dired-hide-details-mode)
:bind ("C-x C-j" . dired-jump)
:general
(dawran/leader-keys
"d" '(dired-jump :which-key "dired"))
:custom
(dired-auto-revert-buffer t)
(dired-dwim-target t)
(dired-recursive-copies 'always)
(dired-recursive-deletes 'always)
(dired-listing-switches "-AFhlv --group-directories-first")
:init
(setq insert-directory-program "gls")
:config
(evil-collection-define-key 'normal 'dired-mode-map
(kbd "C-c C-e") 'wdired-change-to-wdired-mode))
(use-package dired-x
:after dired
:straight nil
:init (setq-default dired-omit-files-p t)
:config
(add-to-list 'dired-omit-extensions ".DS_Store"))
(use-package dired-single
:after dired
:config
(evil-collection-define-key 'normal 'dired-mode-map
"h" 'dired-single-up-directory
"l" 'dired-single-buffer))
(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))
(use-package dired-ranger
:after dired
:config
(evil-collection-define-key 'normal 'dired-mode-map
"y" 'dired-ranger-copy
"X" 'dired-ranger-move
"p" 'dired-ranger-paste))
(use-package dired-subtree
:after dired)
(use-package dired-toggle
:general
(dawran/leader-keys
"td" 'dired-toggle)
:straight nil
:load-path "lisp/")
Shell
Exec-path
(setq exec-path (append exec-path '("/usr/local/bin")))
Vterm
(use-package vterm
:commands vterm
:config
(setq vterm-max-scrollback 10000))
Eshell
(defun dawran/eshell-history ()
"Browse eshell history."
(interactive)
(let ((candidates (cl-remove-duplicates
(ring-elements eshell-history-ring)
:test #'equal :from-end t))
(input (let ((input-start (save-excursion (eshell-bol)))
(input-end (save-excursion (end-of-line) (point))))
(buffer-substring-no-properties input-start input-end))))
(let ((selected (completing-read "Eshell history:"
candidates nil nil input)))
(end-of-line)
(eshell-kill-input)
(insert (string-trim selected)))))
(defun dawran/configure-eshell ()
;; Save command history when commands are entered
(add-hook 'eshell-pre-command-hook 'eshell-save-some-history)
;; Truncate buffer for performance
(add-to-list 'eshell-output-filter-functions 'eshell-truncate-buffer)
;; Use Ivy to provide completions in eshell
(define-key eshell-mode-map (kbd "<tab>") 'completion-at-point)
;; Bind some useful keys for evil-mode
(evil-define-key '(normal insert visual) eshell-mode-map (kbd "C-r") 'dawran/eshell-history)
(evil-define-key '(normal insert visual) eshell-mode-map (kbd "C-a") 'eshell-bol)
(setq eshell-history-size 10000
eshell-buffer-maximum-lines 10000
eshell-hist-ignoredups t
eshell-highlight-prompt t
eshell-scroll-to-bottom-on-input t))
(use-package eshell
:hook (eshell-first-time-mode . dawran/configure-eshell)
:general
(dawran/leader-keys
"e" 'eshell))
(use-package exec-path-from-shell
:defer 1
:init
(setq exec-path-from-shell-check-startup-files nil)
:config
(when (memq window-system '(mac ns x))
(exec-path-from-shell-initialize)))
(with-eval-after-load 'esh-opt
(setq eshell-destroy-buffer-when-process-dies t))
Toggling eshell
(use-package eshell-toggle
:custom
(eshell-toggle-use-git-root t)
(eshell-toggle-run-command nil)
:bind
("C-M-'" . eshell-toggle)
:general
(dawran/leader-keys
"te" 'eshell-toggle))
Development
Project
(use-package project
:commands project-root
:bind
(("s-p" . project-find-file)
("s-P" . project-switch-project))
:init
(defun project-magit-status+ ()
""
(interactive)
(magit-status (project-root (project-current t))))
:custom
(project-switch-commands '((project-find-file "Find file")
(project-find-regexp "Find regexp")
(project-dired "Dired")
(project-magit-status+ "Magit" ?m)
(project-eshell "Eshell"))))
Magit
(use-package magit
:bind ("s-g" . magit-status)
:custom
(magit-diff-refine-hunk 'all)
(magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
:general
(dawran/leader-keys
"g" '(:ignore t :which-key "git")
"gg" 'magit-status
"gb" 'magit-blame-addition
"gd" 'magit-diff-unstaged
"gf" 'magit-file-dispatch
"gl" 'magit-log-buffer-file))
Ripgrep
(use-package rg
:bind ("s-F" . rg-project)
:config
(rg-enable-default-bindings))
LSP
lsp-mode.el
For sentimental reason I actually prefer to use eglot.el over lsp-mode. However, there’s a use case that eglot doesn’t seem to address yet so I switch back to lsp-mode ATM.
(use-package lsp-mode
:hook ((clojure-mode . lsp)
(clojurec-mode . lsp)
(clojurescript-mode . lsp)
(lsp-mode . (lambda () (setq-local idle-highlight-mode nil))))
:custom
(lsp-enable-file-watchers nil)
(lsp-headerline-breadcrumb-enable nil)
(lsp-keymap-prefix "s-l")
(lsp-enable-indentation nil)
(lsp-clojure-custom-server-command '("bash" "-c" "/usr/local/bin/clojure-lsp"))
:config
(lsp-enable-which-key-integration t))
Eglot (disabled)
eglot is a client for Language Server Protocol servers in Emacs. Comparing with lsp-mode, eglot seems to be closer-to-the metal because it chooses to work primarily with Emacs’ built-in libraries:
- definitions can be found via
xref-find-definitions
; - on-the-fly diagnostics are given by
flymake-mode
; - function signature hints are given by
eldoc-mode
; - completion can be summoned with
completion-at-point
. - projects are discovered via
project.el
’s API;
(use-package eglot
:disabled t
:hook ((clojure-mode . eglot-ensure)
(clojurec-mode . eglot-ensure)
(clojurescript-mode . eglot-ensure))
:custom
(eglot-connect-timeout 6000)
:config
(add-to-list 'eglot-server-programs
'((clojure-mode clojurescript-mode) . ("bash" "-c" "clojure-lsp")))
(defun my/project-try-clojure (dir)
"Try to locate a clojure project."
(when-let ((found (clojure-project-dir)))
(cons 'transient found)))
(defun my/eglot--guess-contact-clojure-project-monorepo (orig-fun &rest args)
"Fix project-root for clojure monorepos."
(let ((project-find-functions
(cons 'my/project-try-clojure project-find-functions)))
(apply orig-fun args)))
(advice-add 'eglot--guess-contact :around
#'my/eglot--guess-contact-clojure-project-monorepo))
(use-package flymake
:disabled t
:defer t
:blackout t)
Languages
Clojure
(use-package flycheck-clj-kondo
:disabled t
:defer t)
(use-package clojure-mode
:defer t
:custom
(cljr-magic-requires nil)
:config
;; (require 'flycheck-clj-kondo)
(setq clojure-indent-style 'align-arguments
clojure-align-forms-automatically t))
(use-package clj-refactor
:defer t
:blackout t)
(use-package cider
:custom
(cider-repl-display-help-banner nil)
(cider-repl-display-in-current-window nil)
(cider-repl-pop-to-buffer-on-connect nil)
(cider-repl-use-pretty-printing t)
(cider-repl-buffer-size-limit 100000)
(cider-repl-result-prefix ";; => ")
:config
(add-hook 'cider-repl-mode-hook 'evil-insert-state)
(dawran/localleader-keys
:keymaps '(clojure-mode-map clojurescript-mode-map)
"e" '(:ignore t :which-key "eval")
"eb" 'cider-eval-buffer
"ef" 'cider-eval-defun-at-point
"eF" 'cider-pprint-eval-defun-to-comment
"ee" 'cider-eval-last-sexp
"eE" 'cider-pprint-eval-last-sexp-to-comment
"t" '(:ignore t :which-key "test")
"tt" 'cider-test-run-test
"tn" 'cider-test-run-ns-tests)
:general
(dawran/localleader-keys
:keymaps '(clojure-mode-map clojurescript-mode-map)
"," 'cider))
(use-package clj-refactor
:hook (clojure-mode . clj-refactor-mode))
Go
(use-package go-mode
:mode "\\.go\\'")
Markdown
(use-package markdown-mode
:mode "\\.md\\'"
:hook (markdown-mode . dawran/visual-fill)
:config
(setq markdown-command "marked"))
(use-package markdown-toc
:commands (markdown-toc-generate-toc))
Yaml
(use-package yaml-mode
:mode "\\.\\(e?ya?\\|ra\\)ml\\'")
Syntax Checking with Flycheck
(use-package flycheck
:defer t
;; :hook ((clojure-mode . flycheck-mode)
;; (clojurec-mode . flycheck-mode)
;; (clojurescript-mode . flycheck-mode))
)
Eldoc
(use-package eldoc
:defer t
:blackout t)
Spell Checking Comments and Text
(use-package flyspell
:blackout t
:straight nil
:hook
(prog-mode . flyspell-prog-mode)
(text-mode . flyspell-mode))
Extras
My extra lisp stuffs. Credits to:
(use-package extras
:straight nil
:load-path "lisp/"
:bind
(("M-y" . yank-pop+)
("C-x C-r" . recentf-open-files+)))
App
World Time
(use-package time
:straight nil
:custom
(display-time-world-list '(("Asia/Taipei" "Taipei")
("America/Toronto" "Toronto")
("America/Los_Angeles" "San Francisco")
("Europe/Berlin" "Düsseldorf")
("Europe/London" "GMT")))
:general
(dawran/leader-keys
"tc" #'display-time-world))
RSS
(use-package elfeed
:hook (elfeed-show-mode . dawran/visual-fill)
:custom
(elfeed-feeds '(("https://css-tricks.com/feed/")
("https://dawranliou.com/atom.xml")
"https://ambrevar.xyz/atom.xml"
("http://irreal.org/blog/?feed=rss2" emacs)
("https://emacsredux.com/atom.xml" emacs)))
:general
(dawran/leader-keys
"R" '(elfeed :which-key "RSS")))
Gemini
(use-package elpher
:commands elpher)