dotfiles/common/.config/emacs/init.el.org
2025-01-14 22:05:05 -06:00

18 KiB

Emacs Configuration

Early Setup

Bootstrap package management. I use elpaca with use-package to allow asynchronous declarative package management.

  (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))

Force the use of a custom.el file instead of appending to init.el.

  (setq custom-file (concat user-emacs-directory "custom.el"))
  (when (file-exists-p custom-file)
    (load custom-file))

Make Emacs use the correct PATH variable as macOS fails to load the PATH variable from my login shell.

  (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)))

Appearance

Set up fonts. I use Source Code Pro (Nerd Font) for monospace and Computer Modern for variable width.

  (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))))

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.

  (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)

Disable the default startup screen so Emacs starts in the scratch buffer and also defaults to an empty scratch buffer.

  (setq inhibit-startup-screen t
        initial-scratch-message nil)

Tell Emacs to use line numbers by default.

  (global-display-line-numbers-mode 1)

Configure Emacs to default to spaces over tabs and use a width of 4 by default.

  (setq-default indent-tabs-mode nil)
  (setq tab-width 4
        c-basic-offset tab-width)

Install and configure visual-fill-column to make some file types display with a narrow window centred in the frame.

  (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))

Behaviour

Make Emacs scroll one line at a time instead of big jumps.

  (setq scroll-conservatively most-positive-fixnum)

Make Emacs delete trailing whitspace on save. This does not happen in markdown-mode which sometimes needs trailing whitespace.

  (add-hook 'before-save-hook
            (lambda ()
              (unless (eql (with-current-buffer (current-buffer) major-mode)
                           'markdown-mode)
                (delete-trailing-whitespace))))

Make Emacs create directories if they don't exist if the user selects that answer.

  (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)))))

Disable the creation of backup files which pollute the file system.

  (setq make-backup-files nil)

Make PDFs save where in the document it was last.

  (use-package saveplace-pdf-view
    :config
    (save-place-mode 1))

Configure superior Emacs window management with windmove.

  (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)

Tools

Install esup as a profiling tool.

  (use-package esup
    :config
    (setq esup-depth 0))

Disable ls for dired.

  (setq dired-use-ls-dired nil)

Allow multiple cursors.

  (use-package multiple-cursors
    :bind
    ("C->" . mc/mark-next-like-this)
    ("C-<" . mc/mark-previous-like-this))

Configure dumb-jump for better lookup.

  (use-package dumb-jump
    :init
    (add-hook 'xref-backend-functions #'dumb-jump-xref-activate))

Configure and install magit as a git front end.

  (use-package transient)
  (use-package magit)

Install a better PDF viewer than DocView.

  ;; (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))

Install and configure fzf to be used as a fuzzy finder.

  (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))

Install and configure vterm as a terminal emulator in Emacs.

  (use-package vterm
    :hook
    (vterm-mode . (lambda () (display-line-numbers-mode -1)))
    :bind
    ("C-c v" . vterm))

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.

  (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))

Set up flycheck and flyspell for syntax and spell checking respectively.

  (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)))

Install yasnippet for managing snippets and yasnippet-snippets for a collection of useful snippets.

  (use-package yasnippet
    :init
    (yas-global-mode 1)
    :bind
    ("C-c s" . yas-insert-snippet))
  (use-package yasnippet-snippets)

Install apheleia and clang-format to automatically format code on save.

    (use-package apheleia
      :init (apheleia-global-mode 1))
    (use-package clang-format)

Configure and install elfeed to serve as an rss feed reader. It stores the feed here.

  (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")))

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.

  (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)))))

Install cmake-mode.

  (use-package cmake-mode)

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.

  (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))

Install tools for LaTeX. Namely, auctex for better integration with Emacs and cdlatex for environment and macro insertion.

  (use-package auctex
    :hook
    (LaTeX-mode . (lambda () (put 'LaTeX-mode 'eglot-language-id "latex"))))
  (use-package cdlatex
    :hook
    (LaTeX-mode . turn-on-cdlatex))

Install tools for Emacs Lisp. Namely parinfer-rust-mode which handles parentheses nicely in Emacs Lisp.

  (use-package parinfer-rust-mode
    :hook
    (emacs-lisp-mode . parinfer-rust-mode)
    :init
    (setq parinfer-rust-auto-download t))

Install lua-mode.

  (use-package lua-mode)

Configure how Markdown is displayed (default to variable-width font and use monospace where necessary) and installs markdown-mode.

  (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))))

Install nix-mode.

    (use-package nix-mode
      :mode
      "\\.nix\\'")

Install yaml-mode.

  (use-package yaml-mode)

Set up eglot to run on languages that have been configured.

  (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))))