#+title: Emacs Configuration * Early Setup Bootstrap package management. I use =elpaca= with =use-package= to allow asynchronous declarative package management. #+begin_src emacs-lisp (defvar elpaca-installer-version 0.8) (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory)) (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory)) (defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory)) (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git" :ref nil :depth 1 :files (:defaults "elpaca-test.el" (:exclude "extensions")) :build (:not elpaca--activate-package))) (let* ((repo (expand-file-name "elpaca/" elpaca-repos-directory)) (build (expand-file-name "elpaca/" elpaca-builds-directory)) (order (cdr elpaca-order)) (default-directory repo)) (add-to-list 'load-path (if (file-exists-p build) build repo)) (unless (file-exists-p repo) (make-directory repo t) (when (< emacs-major-version 28) (require 'subr-x)) (condition-case-unless-debug err (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*")) ((zerop (apply #'call-process `("git" nil ,buffer t "clone" ,@(when-let* ((depth (plist-get order :depth))) (list (format "--depth=%d" depth) "--no-single-branch")) ,(plist-get order :repo) ,repo)))) ((zerop (call-process "git" nil buffer t "checkout" (or (plist-get order :ref) "--")))) (emacs (concat invocation-directory invocation-name)) ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch" "--eval" "(byte-recompile-directory \".\" 0 'force)"))) ((require 'elpaca)) ((elpaca-generate-autoloads "elpaca" repo))) (progn (message "%s" (buffer-string)) (kill-buffer buffer)) (error "%s" (with-current-buffer buffer (buffer-string)))) ((error) (warn "%s" err) (delete-directory repo 'recursive)))) (unless (require 'elpaca-autoloads nil t) (require 'elpaca) (elpaca-generate-autoloads "elpaca" repo) (load "./elpaca-autoloads"))) (add-hook 'after-init-hook #'elpaca-process-queues) (elpaca `(,@elpaca-order)) (setq use-package-always-ensure t) (elpaca elpaca-use-package (elpaca-use-package-mode)) #+end_src Force the use of a =custom.el= file instead of appending to =init.el=. #+begin_src emacs-lisp (setq custom-file (concat user-emacs-directory "custom.el")) (when (file-exists-p custom-file) (load custom-file)) #+end_src Make Emacs use the correct =PATH= variable as macOS fails to load the =PATH= variable from my login shell. #+begin_src emacs-lisp (let ((path-from-shell (replace-regexp-in-string "[ \t\n]*$" "" (shell-command-to-string "$SHELL --login -c 'echo $PATH'")))) (setenv "PATH" path-from-shell) (setq exec-path (split-string path-from-shell path-separator))) #+end_src * Appearance Set up fonts. I use Source Code Pro (Nerd Font) for =monospace= and Computer Modern for /variable width/. #+begin_src emacs-lisp (defvar jj/mono-font) (defvar jj/var-font) (pcase system-type (`gnu/linux (setq jj/mono-font "SauceCodePro Nerd Font-11" jj/var-font "CMU Serif-14")) (`darwin (setq jj/mono-font "SauceCodePro Nerd Font-14:weight=thin" jj/var-font "CMU Serif-18"))) (add-to-list 'default-frame-alist `(font . ,jj/mono-font)) (custom-set-faces `(variable-pitch ((t :font ,jj/var-font))) `(fixed-pitch ((t :font ,jj/mono-font)))) #+end_src Use =diredfl= for a colourful =dired= and clean up the interface by removing scroll bars, tool bars, and menu bars. Allow Emacs to scale frames by pixel rather than character. #+begin_src emacs-lisp (use-package diredfl :init (diredfl-global-mode 1)) (use-package ns-auto-titlebar :init (ns-auto-titlebar-mode)) (add-to-list 'default-frame-alist '(vertical-scroll-bars . nil)) ; saves about 0.02 seconds on startup over `(scroll-bar-mode -1)` (push '(tool-bar-lines . 0) default-frame-alist) ; saves about 0.1 seconds on startup over `(tool-bar-mode -1)` (menu-bar-mode -1) (setq frame-resize-pixelwise t) #+end_src Disable the default startup screen so Emacs starts in the =scratch= buffer and also defaults to an empty =scratch= buffer. #+begin_src emacs-lisp (setq inhibit-startup-screen t initial-scratch-message nil) #+end_src Tell Emacs to use line numbers by default. #+begin_src emacs-lisp (global-display-line-numbers-mode 1) #+end_src Configure Emacs to default to spaces over tabs and use a width of 4 by default. #+begin_src emacs-lisp (setq-default indent-tabs-mode nil) (setq tab-width 4 c-basic-offset tab-width) #+end_src Install and configure =visual-fill-column= to make some file types display with a narrow window centred in the frame. #+begin_src emacs-lisp (defun jj/run-visual-line-mode () "run visual-line-mode" (visual-line-mode) (visual-fill-column-mode) (setq visual-fill-column-width 100 visual-fill-column-center-text t)) (use-package visual-fill-column :hook (org-mode . jj/run-visual-line-mode) (markdown-mode . jj/run-visual-line-mode) :config (setq visual-fill-column-width 100 visual-fill-column-center-text t)) #+end_src * Behaviour Make Emacs confirm that I want to close it on kill. #+begin_src emacs-lisp (setq confirm-kill-emacs 'yes-or-no-p) #+end_src Make Emacs scroll one line at a time instead of big jumps. #+begin_src emacs-lisp (setq scroll-conservatively most-positive-fixnum) #+end_src Make Emacs delete trailing whitspace on save. This does not happen in =markdown-mode= which sometimes needs trailing whitespace. #+begin_src emacs-lisp (add-hook 'before-save-hook (lambda () (unless (eql (with-current-buffer (current-buffer) major-mode) 'markdown-mode) (delete-trailing-whitespace)))) #+end_src Make Emacs create directories if they don't exist if the user selects that answer. #+begin_src emacs-lisp (add-to-list 'find-file-not-found-functions (lambda () (let ((parent-directory (file-name-directory buffer-file-name))) (when (and (not (file-exists-p parent-directory)) (y-or-n-p (format "Directory `%s' does not exist! Create it?" parent-directory))) (make-directory parent-directory t))))) #+end_src Disable the creation of backup files which pollute the file system. #+begin_src emacs-lisp (setq make-backup-files nil) #+end_src Make PDFs save where in the document it was last. #+begin_src emacs-lisp (use-package saveplace-pdf-view :config (save-place-mode 1)) #+end_src Configure superior Emacs window management with =windmove=. #+begin_src emacs-lisp (keymap-global-set "C-c w h" 'windmove-left) (keymap-global-set "C-c w j" 'windmove-down) (keymap-global-set "C-c w k" 'windmove-up) (keymap-global-set "C-c w l" 'windmove-right) (keymap-global-set "C-c C-w h" 'windmove-swap-states-left) (keymap-global-set "C-c C-w j" 'windmove-swap-states-down) (keymap-global-set "C-c C-w k" 'windmove-swap-states-up) (keymap-global-set "C-c C-w l" 'windmove-swap-states-right) #+end_src * Tools Install =esup= as a profiling tool. #+begin_src emacs-lisp (use-package esup :config (setq esup-depth 0)) #+end_src Disable =ls= for =dired=. #+begin_src emacs-lisp (setq dired-use-ls-dired nil) #+end_src Allow multiple cursors. #+begin_src emacs-lisp (use-package multiple-cursors :bind ("C->" . mc/mark-next-like-this) ("C-<" . mc/mark-previous-like-this)) #+end_src Configure =dumb-jump= for better lookup. #+begin_src emacs-lisp (use-package dumb-jump :init (add-hook 'xref-backend-functions #'dumb-jump-xref-activate)) #+end_src Configure and install =magit= as a =git= front end. #+begin_src emacs-lisp (use-package transient) (use-package magit) #+end_src Install a better PDF viewer than =DocView=. #+begin_src emacs-lisp (use-package pdf-tools :hook (doc-view-mode . (lambda () (pdf-tools-install))) ;; install on first pdf opened instead of startup (pdf-view-mode . (lambda () (display-line-numbers-mode -1))) :init (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer) :config (setq TeX-view-program-selection '((output-pdf "PDF Tools")) TeX-view-program-list '(("PDF Tools" TeX-pdf-tools-sync-view)) TeX-source-correlate-start-server t)) #+end_src Install and configure =fzf= to be used as a fuzzy finder. #+begin_src emacs-lisp (use-package fzf :bind ("C-c C-f" . fzf) :config (setq fzf/args "-x --color 16 --print-query --margin=1,0 --no-hscroll" fzf/executable "fzf" fzf/git-grep-args "-i --line-number %s" fzf/grep-command "grep -nrH" fzf/position-bottom nil fzf/window-height 15)) #+end_src Install and configure =vterm= as a terminal emulator in Emacs. #+begin_src emacs-lisp (use-package vterm :hook (vterm-mode . (lambda () (display-line-numbers-mode -1))) :bind ("C-c v" . vterm)) #+end_src Install =company= for completions. It is configured to start with no delay immediately after the first key press. =vertico= is used as a front end for completions. =orderless= is used to allow searching in any portion of a string and =marginalia= gives descriptions of items in the list. #+begin_src emacs-lisp (use-package company :init (global-company-mode) :config (setq company-idle-delay 0.4 company-minimum-prefix-length 1 company-selection-wrap-around t)) (use-package vertico :custom (vertico-cycle t) :init (vertico-mode 1)) (use-package orderless :custom (completion-styles '(orderless basic)) (completion-category-overrides '((file (styles basic partial-completion))))) (use-package marginalia :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :init (marginalia-mode 1)) #+end_src Set up =flycheck= and =flyspell= for syntax and spell checking respectively. #+begin_src emacs-lisp (use-package flycheck :config (add-hook 'after-init-hook #'global-flycheck-mode)) (use-package flyspell-correct :hook (text-mode . flyspell-mode) :bind (:map flyspell-mode-map ("C-;" . flyspell-correct-wrapper))) #+end_src Install =yasnippet= for managing snippets and =yasnippet-snippets= for a collection of useful snippets. #+begin_src emacs-lisp (use-package yasnippet :init (yas-global-mode 1) :bind ("C-c s" . yas-insert-snippet)) (use-package yasnippet-snippets) #+end_src Install =apheleia= and =clang-format= to automatically format code on save. #+begin_src emacs-lisp (use-package apheleia :init (apheleia-global-mode 1)) (use-package clang-format) #+end_src Configure and install =elfeed= to serve as an =rss= feed reader. It stores the feed [[./feed.org.org][here]]. #+begin_src emacs-lisp (use-package elfeed :bind ("C-c e f" . elfeed) ("C-c e u" . elfeed-update)) (use-package elfeed-goodies :after elfeed :config (elfeed-goodies/setup)) (use-package elfeed-org :config (elfeed-org) (setq rmh-elfeed-org-files (list "~/.config/emacs/feed.org"))) #+end_src * Languages Configure =org-mode=. I use =~/org= as my =org= directory and hide emphasis markers because it's much easier to read that way. I enable =org-crypt= to allow reading and writing encrypted =org= files. I also replace bullets in bulleted lists with nicer looking icons. I configure faces to default to variable-width font, but switching to monospace where it is necessary. Finally, I use =visual-fill-column= to make =org= files display with a relatively narrow window centred in the frame. #+begin_src emacs-lisp (use-package org :hook (org-mode . (lambda () (variable-pitch-mode) (display-line-numbers-mode -1))) :config (org-crypt-use-before-save-magic) (setq org-directory "~/org" org-hide-emphasis-markers t org-format-latex-options (plist-put org-format-latex-options :scale 2.0) org-return-follows-link t org-tags-exclude-from-inheritance '("crypt") org-crypt-key nil auto-save-default nil) (font-lock-add-keywords 'org-mode '(("^ *\\([-]\\) " (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•")))))) :custom-face (org-block ((t :font ,jj/mono-font))) (org-code ((t :font ,jj/mono-font (:inherit (shadow))))) (org-document-info-keyword ((t :font ,jj/mono-font (:inherit (shadow))))) (org-meta-line ((t :font ,jj/mono-font (:inherit (font-lock-comment-face))))) (org-verbatim ((t :font ,jj/mono-font (:inherit (shadow))))) (org-table ((t :font ,jj/mono-font (:inherit (shadow))))) (org-document-title ((t (:inherit title :height 2.0 :underline nil)))) (org-level-1 ((t (:inherit outline-1 :weight bold :height 1.75)))) (org-level-2 ((t (:inherit outline-2 :weight bold :height 1.5)))) (org-level-3 ((t (:inherit outline-3 :weight bold :height 1.25)))) (org-level-4 ((t (:inherit outline-4 :weight bold :height 1.1)))) (org-level-5 ((t (:inherit outline-5 :height 1.1)))) (org-level-6 ((t (:inherit outline-6))))) #+end_src Install =cmake-mode=. #+begin_src emacs-lisp (use-package cmake-mode) #+end_src Install =go-mode= and tools for =go= source code. Namely, =go-eldoc= gets documentation for =go= variables, functions, and arguments, =go-gen-tests= automatically generates tests for =go= code, and =go-guru= helps with refactoring =go= code. #+begin_src emacs-lisp (use-package go-mode) (use-package go-eldoc :hook (go-mode . go-eldoc-setup)) (use-package go-gen-test) (use-package go-guru :hook (go-mode . go-guru-hl-identifier-mode)) #+end_src Install tools for LaTeX. Namely, =auctex= for better integration with Emacs and =cdlatex= for environment and macro insertion. #+begin_src emacs-lisp (use-package auctex :hook (LaTeX-mode . (lambda () (put 'LaTeX-mode 'eglot-language-id "latex")))) (use-package cdlatex :hook (LaTeX-mode . turn-on-cdlatex)) #+end_src Install tools for Emacs Lisp. Namely =parinfer-rust-mode= which handles parentheses nicely in Emacs Lisp. #+begin_src emacs-lisp :tangle yes (use-package parinfer-rust-mode :hook (emacs-lisp-mode . parinfer-rust-mode) :init (setq parinfer-rust-auto-download t)) #+end_src Install =lua-mode=. #+begin_src emacs-lisp (use-package lua-mode) #+end_src Configure how Markdown is displayed (default to variable-width font and use monospace where necessary) and installs =markdown-mode=. (Markdown faces seem to be broken on Emacs 30). #+begin_src emacs-lisp (use-package markdown-mode :hook (markdown-mode . (lambda () (variable-pitch-mode) (display-line-numbers-mode -1))) :config (setq markdown-hide-markup t) :custom-face (markdown-header-face ((t :font ,jj/var-font :weight bold))) (markdown-header-face-1 ((t (:inherit markdown-header-face :height 2.0)))) (markdown-header-face-2 ((t (:inherit markdown-header-face :height 1.75)))) (markdown-header-face-3 ((t (:inherit markdown-header-face :height 1.5)))) (markdown-header-face-4 ((t (:inherit markdown-header-face :height 1.25)))) (markdown-header-face-5 ((t (:inherit markdown-header-face :height 1.1)))) (markdown-header-face-6 ((t (:inherit markdown-header-face :height 1.1)))) (markdown-blockquote-face ((t :font ,jj/var-font))) (markdown-code-face ((t :font ,jj/mono-font))) (markdown-html-attr-name-face ((t :font ,jj/mono-font))) (markdown-html-attr-value-face ((t :font ,jj/mono-font))) (markdown-html-entity-face ((t :font ,jj/mono-font))) (markdown-html-tag-delimiter-face ((t :font ,jj/mono-font))) (markdown-html-tag-name-face ((t :font ,jj/mono-font))) (markdown-html-comment-face ((t :font ,jj/mono-font))) (markdown-header-delimiter-face ((t :font ,jj/mono-font))) (markdown-hr-face ((t :font ,jj/mono-font))) (markdown-inline-code-face ((t :font ,jj/mono-font))) (markdown-language-info-face ((t :font ,jj/mono-font))) (markdown-language-keyword-face ((t :font ,jj/mono-font))) (markdown-link-face ((t :font ,jj/mono-font))) (markdown-markup-face ((t :font ,jj/mono-font))) (markdown-math-face ((t :font ,jj/mono-font))) (markdown-metadata-key-face ((t :font ,jj/mono-font))) (markdown-metadata-value-face ((t :font ,jj/mono-font))) (markdown-missing-link-face ((t :font ,jj/mono-font))) (markdown-plain-url-face ((t :font ,jj/mono-font))) (markdown-reference-face ((t :font ,jj/mono-font))) (markdown-table-face ((t :font ,jj/mono-font))) (markdown-url-face ((t :font ,jj/mono-font)))) #+end_src Install =nix-mode=. #+begin_src emacs-lisp (use-package nix-mode :mode "\\.nix\\'") #+end_src Install =yaml-mode=. #+begin_src emacs-lisp :tangle yes (use-package yaml-mode) #+end_src Set up =eglot= to run on languages that have been configured. #+begin_src emacs-lisp (global-set-key (kbd "C-c r") 'eglot-rename) (global-set-key (kbd "C-c a") 'eglot-code-actions) (use-package tree-sitter) (use-package tree-sitter-langs) (dolist (lang-hook '(sh-mode-hook c-mode-hook c++-mode-hook cc-mode-hook cmake-mode-hook html-mode-hook css-mode-hook js-json-mode-hook js-mode-hook python-mode-hook go-mode-hook lua-mode-hook ;; markdown-mode-hook tex-mode-hook LaTeX-mode-hook yaml-mode-hook nix-mode-hook)) (add-hook lang-hook (lambda () (eglot-ensure) (tree-sitter-mode 1) (tree-sitter-hl-mode 1)))) #+end_src