nixos-config/home-manager/modules/emacs/config.org

57 KiB

Emacs config

This is my emacs configuration. It is structured as an org-mode file, to easily provide context and documentation.

Last export:

Setup

Early init

This ends up in early-init.el.

  ;; -*- lexical-binding: t; -*-
  ;; Prevent package.el from loading packages until we're ready for it
  (setq package-enable-at-startup nil)
  ;; Don't resize the frame to preserve the number of columns or lines
  ;; being displayed when setting font, menu bar, tool bar, tab bar,
  ;; internal borders, fringes, or scroll bars.  Since I use a tiling
  ;; window manager, this option is 1. useless anyways and 2. _terribly_ expensive.
  (setq frame-inhibit-implied-resize t)
  ;; Suppress warnings and errors during asynchronous native compilation
  (setq native-comp-async-report-warnings-errors nil)
  ;; Prevent unwanted runtime builds; packages are compiled ahead-of-time when
  ;; they are installed and site files are compiled when gccemacs is installed.
  ;; (setq comp-deferred-compilation nil)
  (setq native-comp-deferred-compilation nil)

  ;; Prevent unwanted runtime builds in gccemacs (native-comp); packages are
  ;; compiled ahead-of-time when they are installed and site files are compiled
  ;; when gccemacs is installed.
  (setq comp-deferred-compilation nil)

  ;; This should fix input lag by ~80% when using PGTK
  (setq-default pgtk-wait-for-event-timeout 0)

Enable lexical binding

Setting lexical-binding to t can improve startup time. This has to be first!

  ;; -*- lexical-binding: t; -*-

Personal variables

This sets some variables with my personal preferences for easy customization

  (defvar my/default-font "RecMonoLinear Nerd Font")
  (defvar my/variable-width-font "Iosevka Aile")
  (defvar my/comment-font "RecMonoCasual Nerd Font")
  (defvar my/default-font-height 120)
  (defvar my/default-font-weight 'light)
  (defvar my/default-font-width 'normal)
  (defvar my/variable-width-font-height 1.1)
  (defvar my/config-file-path (expand-file-name "config.org" user-emacs-directory))
  (defvar my/snippets-dir (expand-file-name "snippets" user-emacs-directory))
  (defvar my/local-init-file (expand-file-name "local-init.el" user-emacs-directory))
  (defvar my/leader "C-c")

And my personal info.

  (setq user-full-name "Erwin Boskma"
        user-mail-address "erwin@datarift.nl")

Garbage collector

Increasing the garbage collector threshold should also improve startup time. This increases it from 800 kB to 128MB

  (setq gc-cons-threshold (* 128 1024 1024))

This resets the threshold back to it's default. This should not be done if lsp-mode is enabled, it needs the higher threshold.

  (add-hook 'after-init-hook
            (lambda ()
              (setq gc-cons-threshold 800000)
              (message "gc-cons-threshold restored to %s"
                       gc-cons-threshold)))

Increase process output buffer

LSP responses can be rather large, in the 800KiB - 3MiB range. 2MiB is a decent value

  (setq read-process-output-max (* 2 1024 1024))

Package sources

Add repositories where packages are installed from.

    (setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                             ("nongnu" . "https://elpa.nongnu.org/nongnu/")
                             ("melpa" . "https://melpa.org/packages/")
                             ("org" . "https://orgmode.org/elpa/")
                             ("onpa" . "https://olanilsson.bitbucket.io/packages/")))

Bootstrap use-package

If use-package is not installed, install it.

NOTE: Disabled because it is builtin since emacs 29

  (require 'package)
  (package-initialize)
  (unless (package-installed-p 'use-package)
    (package-refresh-contents)
    (package-install 'use-package)
    (cl-eval-when 'compile (require 'use-package)))

By default packages should always be installed from the package manager. This is equivalent to setting :ensure t on each call to use-package. To override this for a package (e.g. because it is builtin, or a subpackage), use :ensure nil. This is done automatically for packages that use :load-path.

  (setq use-package-always-ensure t)

general.el

general.el provides a more convenient way for binding keys in emacs. It also integrates with use-package with the :general keyword.

  (use-package general
    :demand t
    :config
    (progn
      (general-define-key
       :prefix my/leader
       "a" 'org-agenda
       "k" 'general-describe-keybindings)
      (general-define-key
       "C-M-z" 'zap-to-char
       "M-z" 'zap-up-to-char)))

Disable the customize interface

The customize functionality is annoying and messes up regularly. Stuff it has done so far:

  • Clobber the path to mix in Elixir projects

This has been borrowed from Doom Emacs

  (dolist (sym '(customize-option customize-browse customize-group customize-face
                                  customize-rogue customize-saved customize-apropos
                                  customize-changed customize-unsaved customize-variable
                                  customize-set-value customize-customized customize-set-variable
                                  customize-apropos-faces customize-save-variable
                                  customize-apropos-groups customize-apropos-options
                                  customize-changed-options customize-save-customized))
    (put sym 'disabled (concat "This config doesn't support `customize', configure Emacs from " user-emacs-directory "/config.org instead")))
  (put 'customize-themes 'disabled (concat "Use `load-theme' in " user-emacs-directory "/config.org instead"))

Custom packages location

Emacs only includes files directly under user-emacs-directory. I like to keep custom packages in a separate elisp/ subdirectory.

  (add-to-list 'load-path (expand-file-name "elisp/" user-emacs-directory))

Record key frequency

Records what keys are used the most, so I can see if I can optimise shortcuts

  (use-package keyfreq
    :config
    (keyfreq-mode 1)
    (keyfreq-autosave-mode 1))

Save minibuffer history

  (use-package savehist
    :ensure nil
    :init
    (savehist-mode))

Preferences

Don't display the help screen at startup

  (setq inhibit-startup-screen t)

Enable line wrapping

  (global-visual-line-mode 1)

Disable title bar, toolbar and scroll bar

  ; This needs to go before scroll-bar-mode
  (setq-default default-frame-alist '((undecorated . t)))
  (tool-bar-mode -1)
  (menu-bar-mode -1)
  (scroll-bar-mode -1)

Don't show the warnings buffer for anything lesser than an error.

  (setq warning-minimum-level :error)

Keep buffers up-to-date automatically

  (global-auto-revert-mode t)

Automatically scroll the compile buffer

  (setq compilation-scroll-output 'first-error)

Always display line numbers, and the relative line number

  (setq display-line-numbers-type 'relative)
  (global-display-line-numbers-mode)

Show matching parenthesese

  (show-paren-mode 1)
  (setq show-paren-style 'expression)

Surround marked text with (), [] or {}

  (electric-pair-mode 1)

Centralise backup files (the filename~ files), so they don't pollute the project.

  (setq backup-directory-alist '((".*" . "~/.local/share/emacs/backup"))
        backup-by-copying t   ; Don't delink hardlinks
        version-control t     ; Use version numbers on backup files
        delete-old-versions t ; Automatically delete obsolete backups
        kept-new-versions 20  ; How many of the newest versions to keep
        kept-old-versions 5   ; And how many of the oldest
        )

Same for auto save files (the ones with the #)

  (setq auto-save-file-name-transforms '((".*" "~/.local/share/emacs/autosave/" t)))

Default indenting to spaces. If tabs are necessary, this can be set buffer-local. A single tab character can be added using C-q TAB

  (setq-default indent-tabs-mode nil)

Show trailing whitespace

  (setq show-trailing-whitespace t)

Delete trailing whitespace on save.

  (add-hook 'before-save-hook 'delete-trailing-whitespace)

Sentences end in one space, not two. We're not using typewriters.

  (setq sentence-end-double-space nil)

Don't move files to trash when deleting.

  (setq delete-by-moving-to-trash nil)

Restore cursor position when re-opening a file

  (save-place-mode t)

Prefer to open frames in a horizontal split and make sure they're of a decent width

  (setq split-height-threshold nil
        window-min-width 100)

Set fill column to 80

  (setq-default fill-column 80)

Kill whole lines instead of clearing them

  (setq kill-whole-line t)

Interface

Easy edit config file

Disabled because the configuration is handled by Nix using emacs-overlay

  (defun find-config ()
    "Edit config.org"
    (interactive)
    (find-file my/config-file-path))

  (general-define-key
       :prefix my/leader
       "c" 'find-config)

Appearance

Enable pixel scrolling.

  (setq pixel-scroll-precision-mode t)

I like the dracula theme

  (use-package dracula-theme
    :init
    (load-theme 'dracula :no-confirm))

Monokai Pro is also pretty.

  (use-package monokai-pro-theme
    :init
    (load-theme 'monokai-pro-spectrum :no-confirm))

So is Catppuccin

    (use-package catppuccin-theme
      :init
      (setq catppuccin-flavor 'mocha)
      (load-theme 'catppuccin :no-confirm))

Set fonts.

  (defun my/set-font-size (&optional frame)
    (let* ((frame (or frame (selected-frame)))
           (geometry (frame-monitor-attribute 'geometry frame))
           (mm-size (frame-monitor-attribute 'mm-size frame))
           (width-px (caddr geometry))
           (width-mm (car mm-size))
           (width-in (/ width-mm 25.4))
           (display-dpi (/ width-px width-in))
           (font-height (cond
                         ((< display-dpi 110) 120)
                         ((< display-dpi 130) 140)
                         ((< display-dpi 160) 160)
                         (t 160))))
      (set-face-attribute 'default frame :height font-height)))

  (add-hook 'server-after-make-frame-hook 'my/set-font-size)
  (add-hook 'after-make-frame-functions 'my/set-font-size)

  (defun my/setup-fonts ()
    (set-face-attribute 'default nil :family my/default-font :weight 'light)
    (set-face-attribute 'font-lock-comment-face nil :font my/comment-font)
    (set-face-attribute 'variable-pitch nil
                        :font my/variable-width-font
                        :height my/variable-width-font-height))
  (add-hook 'after-init-hook 'my/setup-fonts)

Emoji support

    (defun set-emoji-font ()
      (when (member "Twitter Color Emoji" (font-family-list))
        (set-fontset-font t 'emoji (font-spec :family "Twitter Color Emoji") nil 'prepend)))

    (use-package emojify
      :defer t
      :init
      (setq emojify-display-style 'unicode)
      :config
      (set-emoji-font)
      :hook (server-after-make-frame . set-emoji-font))

Enable ligatures

  (setq iosevka-ligatures '("<---" "<--"  "<<-" "<-" "->" "-->" "--->" "<->" "<-->" "<--->" "<---->" "<!--"
                                         "<==" "<===" "<=" "=>" "=>>" "==>" "===>" ">=" "<=>" "<==>" "<===>" "<====>" "<!---"
                                         "<~~" "<~" "~>" "~~>" "::" ":::" "==" "!=" "===" "!=="
                                         ":=" ":-" ":+" "<*" "<*>" "*>" "<|" "<|>" "|>" "+:" "-:" "=:" "<******>" "++" "+++"))
  (setq monaspace-ligatures '(
                                          ; ss01
                              "==" "===" "=/=" "!=" "!==" "/=" "/==" "~~" "=~" "!~"
                                          ; ss02
                              ">=" "<="
                                          ; ss03
                              "->" "<-" "=>" "<!--" "-->" "<~" "<~~" "~>" "~~>" "<~>"
                                          ; ss04
                              "</" "/>" "</>" "/\\" "\\/"
                                          ; ss05
                              "|>" "<|"
                                          ; ss06
                              "##" "###"
                                          ; ss07
                              "***" "/*" "*/" "/*/" "(*" "*)" "(*)"
                                          ; ss08
                              ".=" ".-" "..<"
                                          ; dlig & calt
                              "<!" "**" "::" "=:" "=!" "=/" "--" ".." "//" "&&" "||" ":=" ":>" ":<" "!!" ">:" "<:" "#=" "?:" "?." "??" ";;" "///" ":::" "..." "=!=" "=:=" "..=" "..-"))

  (use-package ligature
    :config
    ;; Enable all Iosevka ligatures in programming modes
    (ligature-set-ligatures 'prog-mode iosevka-ligatures)
    ;; Ligatures for Monaspace
    ;; Enables ligature checks globally in all buffers. You can also do it
    ;; per mode with `ligature-mode'.
    (global-ligature-mode t))

Add a dashboard on startup

  (use-package dashboard
    :after all-the-icons
    :hook (dashboard-mode . (lambda ()
                              (setq show-trailing-whitespace nil)))
    :config
    (setq dashboard-set-navigator t
          dashboard-center-content t
          dashboard-set-file-icons t
          dashboard-set-heading-icons t
          dashboard-set-init-info t
          dashboard-image-banner-max-height 250
          dashboard-banner-logo-title "Bûter, brea en griene tsiis, wa dat net sizze kin is gjin oprjochte Fries."
          dashboard-startup-banner (concat user-emacs-directory "images/ue-colorful.png")
          dashboard-footer-icon (all-the-icons-octicon "dashboard"
                                                 :height 1.1
                                                 :v-adjust -0.05
                                                 :face 'font-lock-keyword-face))
    (setq dashboard-navigator-buttons
          `(((,(all-the-icons-octicon "search" :height 0.9 :v-adjust -0.1)
               " Find file" nil
               (lambda (&rest _) (find-file)) nil "" "            C-x C-f"))
            ((,(all-the-icons-octicon "file-directory" :height 1.0 :v-adjust -0.1)
               " Open project" nil
               (lambda (&rest _) (projectile-switch-project)) nil "" "         C-c p p"))
            ((,(all-the-icons-octicon "three-bars" :height 1.1 :v-adjust -0.1)
               " File explorer" nil
               (lambda (&rest _) (projectile-dired)) nil "" "        C-c p D"))
            ((,(all-the-icons-octicon "settings" :height 0.9 :v-adjust -0.1)
               " Open settings" nil
               (lambda (&rest _) (find-config)) nil "" "        C-c C  "))))
    (setq
     dashboard-projects-backend 'projectile
     dashboard-items '((recents . 5)
                       (projects . 5)
                       (registers . 5)))
    (dashboard-setup-startup-hook)
    ;; Also show dashboard on new emacsclient window
    (setq initial-buffer-choice (lambda ()
                                  (get-buffer-create "*dashboard*")))
    :custom-face
    (dashboard-heading ((t (:weight bold)))))

Modeline

diminish hides modes from the modeline

  (use-package diminish)

all-the-icons provides ALL the icons. Run `all-the-icons-install-fonts` after installing to download the actual font.

  (use-package all-the-icons
    :if (display-graphic-p))

minions adds a menu for minor modes to the modeline

  (use-package minions
    :config
    (minions-mode 1))

Use doom-modeline for a nice and fancy modeline 2023-05-12 Disabled because it causes emacs to hang

  (use-package doom-modeline
    :init
    (doom-modeline-mode 1)
    :config
    (setq doom-modeline-height 25
          doom-modeline-bar-width 6
          doom-modeline-lsp t
          doom-modeline-github nil
          doom-modeline-mu4e nil
          doom-modeline-irc nil
          doom-modeline-minor-modes t
          doom-modeline-persp-name nil
          doom-modeline-buffer-file-name-style 'truncate-except-project
          doom-modeline-icon t)
    :custom-face
    (mode-line ((t (:height 0.85))))
    (mode-line-inactive ((t (:height 0.85)))))

moody cleans up the modeline a bit so it is nicer to look at

  (use-package moody
    :config
    (setq x-underline-at-descent-line t)
    (moody-replace-mode-line-buffer-identification)
    (moody-replace-vc-mode)
    (moody-replace-eldoc-minibuffer-message-function))

Command completion

Ivy / Counsel

Disabled in favor of vertico

Ivy is a generic completion framework which uses the minibuffer. ivy GitHub page

  (use-package ivy
    :diminish t
    :bind
    <<ivy-binds>>
    :config
    <<ivy-config>>
    )

Ivy config

    (ivy-mode t)
    (setq ivy-initial-inputs-alist nil
          ivy-use-virtual-buffers nil)
    (define-key read-expression-map (kbd "C-r") 'counsel-expression-history)

Ivy keybindings

  ("C-x s" . swiper)
  ("C-x C-r" . ivy-resume)

Counsel enhances emacs commands with ivy. counsel GitHub page

  (use-package counsel
    :bind
    ("M-x" . counsel-M-x)
    ("C-x C-m" . counsel-M-x)
    ("C-x C-f" . counsel-find-file)
    ("C-x c k" . counsel-yank-pop))

Company provides completion in the main buffer. company website

  (use-package company
    :diminish t
    :hook
    (after-init . global-company-mode))

Hydra allows you to group commands behind a custom prefix. hydra GitHub page

  (use-package ivy-hydra)

major-mode-hydra binds a single key to open a context sensitive hydra based on the current major mode. Hydras can be defined in use-package definitions via the :mode-hydra integration.

  (use-package major-mode-hydra
    :bind
    ("C-M-SPC" . major-mode-hydra)
    :config
    (major-mode-hydra-define org-mode
                             ()
                             ("Tools"
                              (("l" org-lint "lint")))))

Vertico / Corfu

vertico is a minimalistic completion UI based on the default completion system integrated in emacs.

  (use-package vertico
    :custom
    (vertico-cycle t)
    :init
    (vertico-mode))

marginalia adds extra information to the minibuffer completions.

  (use-package marginalia
    :after vertico
    :custom
    (marginalia-annotaters '(marginalia-annotaters-heavy marginalia-annotaters-light nil))
    :init
    (marginalia-mode))

consult is an alternative to counsel for ivy, but for vertico.

  (use-package consult
    :general
    ("C-s" 'consult-line)
    ("C-x c k" 'consult-yank-pop)
    :config
    (setq consult-project-function (lambda ()
                                     (when (fboundp 'projectile-project-root)
                                       (projectile-project-root)))))

corfu enhances the completion at point function popups

      (use-package corfu
        ;; :bind
        ;; ("TAB" . corfu-insert)
        :custom
        (corfu-cycle t)
        (corfu-auto t)
        :init
        (global-corfu-mode))

orderless is a flexible completion style that allows flexible matching using literal strings, regex and more.

  (use-package orderless
    :init
    (setq completion-styles '(orderless partial-completion basic)
          ;; completion-category-defaults nil
          completion-category-overrides '((file (styles basic partial-completion)))))

Use corfu with dabbrev (included with emacs)

  (use-package dabbrev
    :ensure nil
    :general
    ("M-/" 'dabbrev-completion)
    ("C-M-/" 'dabbrev-expand)
    :custom
    (dabbrev-ignored-buffer-regexps '((rx ".(?:pdf|jpe?g|png)" eos))))

Misc

which-key shows suggestions when you type an incomplete command and wait for a bit (1 second by default). which-key GitHub page

  (use-package which-key
    :defer 0
    :diminish which-key-mode
    :config
    (which-key-mode)
    (setq which-key-idle-delay 1))

Possible candidates

The following packages look interesting, and are worth investigating further:

File tree

treemacs is Emacs' equivalent of neovim's NeoTree or vim's NERDTree.

  (use-package treemacs
    :defer t
    :init
    (with-eval-after-load 'winum
      (define-key winum-keymap (kbd "M-0") #'treemacs-select-window))
    :general
    ("M-0" 'treemacs-select-window)
    (:prefix "C-c"
     "t 1"   'treemacs-delete-other-windows
     "t t"   'treemacs
     "t d"   'treemacs-select-directory
     "t B"   'treemacs-bookmark
     "t C-t" 'treemacs-find-file
     "t M-t" 'treemacs-find-tag)
    :config
    (progn
      (treemacs-project-follow-mode)))

Enable integration with projectile

  (use-package treemacs-projectile
    :after (treemacs projectile))

Add icons

  (use-package treemacs-icons-dired
    :hook (dired-mode . treemacs-icons-dired-enable-once))

Enable magit integration

  (use-package treemacs-magit
    :after (treemacs magit))

Programming

General settings

Auto balance parenthesese. Note: Disabled in favour of electric-pair-mode

  (use-package smartparens
    :ghook 'prog-mode-hook)

Rainbow delimiters FTW!

  (use-package rainbow-delimiters
    :ghook 'prog-mode-hook)

Paredit is a minor mode for editing parentheses

  (use-package paredit
    :hook (emacs-lisp-mode . paredit-mode)
    (lisp-mode . paredit-mode)
    (racket-mode . paredit-mode)
    (racket-repl-mode . paredit-mode)
    :general
    (:keymaps 'paredit-mode-map
              "{" 'paredit-open-curly
              "}" 'paredit-close-curly
              "M-[" 'paredit-wrap-square
              "M-{" 'paredit-wrap-curly))

Project management

Projectile works great to manage projects. It includes fuzzy search and supports jumping between files (e.g. .h <-> .cpp). projectile GitHub page

  (use-package projectile
    :diminish projectile-mode
    :config
    ;; (setq projectile-completion-system 'ivy)
    (projectile-mode)
    :general
    (:prefix "C-c"
     "p" '(:keymap projectile-command-map))
    :init
    (setq projectile-switch-project-action #'projectile-dired
          projectile-project-search-path '("~/workspace" "~/workspace/horus" "~/workspace/horus/web" "~/workspace/horus/horus-vr")))

There is also an integration with counsel.

  (use-package counsel-projectile
    :after projectile
    :general
    ("C-SPC" 'counsel-projectile-switch-project)
    :config
    (counsel-projectile-mode))

Enable ripgrep support

  (use-package rg
    :after projectile
    :config
    (rg-enable-default-bindings))

TRAMP

Set some connection properties

  (with-eval-after-load "tramp" (add-to-list 'tramp-connection-properties
                                             (list (regexp-quote "/sshx:hass:")
                                                   "remote-shell" "/bin/bash")))

Ollama

Let's evaluate this puppy.

      (use-package gptel
        :general
        (:prefix my/leader
                 "m" 'gptel-menu
                 "C-m" 'gptel-send)
        :config
        (setq gptel-model "mistral-nemo"
              gptel-backend (gptel-make-ollama "Ollama"
                              :host "100.119.162.110:11434"
                              :stream t
                              :models '("mistral" "mistral-nemo")))
        (add-to-list 'gptel-directives '(commit . "You are an expert programmer summarizing a git diff.
  Reminders about the git diff format:
  For every file, there are a few metadata lines, like (for example):
  ```
  diff --git a/lib/index.js b/lib/index.js
  index aadf691..bfef603 100644
  --- a/lib/index.js
  +++ b/lib/index.js
  ```
  This means that `lib/index.js` was modified in this commit. Note that this is only an example.
  Then there is a specifier of the lines that were modified.
  A line starting with `+` means it was added.
  A line that starting with `-` means that line was deleted.
  A line that starts with neither `+` nor `-` is code given for context and better understanding.
  It is not part of the diff.
  After the git diff of the first file, there will be an empty line, and then the git diff of the next file.

  Do not include the file name as another part of the comment.
  Do not use the characters `[` or `]` in the summary.
  Write every summary comment in a new line.
  Comments should be in a bullet point list, each line starting with a `-`.
  The summary should not include comments copied from the code.
  The output should be easily readable. When in doubt, write fewer comments and not more. Do not output comments that
  simply repeat the contents of the file.
  Readability is top priority. Write only the most important comments about the diff.

  EXAMPLE SUMMARY COMMENTS:
  ```
  - Raise the amount of returned recordings from `10` to `100`
  - Fix a typo in the github action name
  - Move the `octokit` initialization to a separate file
  - Add an OpenAI API for completions
  - Lower numeric tolerance for test files
  - Add 2 tests for the inclusive string split function
  ```
  Most commits will have less comments than this examples list.
  The last comment does not include the file names,
  because there were more than two relevant files in the hypothetical commit.
  Do not include parts of the example in your summary.
  It is given only as an example of appropriate comments.
  ")))

Git

Magit. Obviously. magit website

  (use-package magit
    :config
    ;; (setq magit-completing-read-function 'ivy-completing-read)
    :general
    (:prefix my/leader
     "g s" 'magit-status
     "g x" 'magit-checkout
     "g c" 'magit-commit
     "g p" 'magit-push
     "g u" 'magit-pull
     "g e" 'magit-ediff-resolve
     "g r" 'magit-rebase-interactive
     "g i" 'magit-init))

transient can be used to create command dispatchers. Magit uses it to easily add options to git commands, and it displays a nice popup with the possible flags and commands. transient manual

  (use-package transient)

Visualise git changes in the gutter, next to the line numbers git-gutter GitHub page

  (use-package git-gutter
    :diminish t
    :ghook
    'prog-mode-hook
    'org-mode-hook
    :config
    (setq git-gutter:update-interval 0.02))

  (use-package git-gutter-fringe
    :config
    (define-fringe-bitmap 'git-gutter-fr:added [224] nil nil '(center repeated))
    (define-fringe-bitmap 'git-gutter-fr:modified [224] nil nil '(center repeated))
    (define-fringe-bitmap 'git-gutter-fr:deleted [128 192 224 240] nil nil 'bottom))

Show inline git-blame with blamer.el. Inspired by VS Code's Git Lens

  (use-package blamer
    :after bind-key
    :bind (("C-c C-i" . blamer-show-commit-info)
           ("C-c i" . blamer-show-posframe-commit-info))
    :config
    (global-blamer-mode 1))

Syntax checking and highlighting

Flycheck

Flycheck is a general purpose syntax highlighting framework that provides hooks for other packages and an improvement of the builtin flymake. website

  (use-package flycheck
    :diminish t
    :init
    (global-flycheck-mode))

Add eglot support for flycheck-mode

  (use-package flycheck-eglot
    :after (flycheck eglot)
    :config
    (global-flycheck-eglot-mode 1))

Tree-sitter

tree-sitter is a new development in parsing and syntax highlighting. It has been merged into Emacs 29, but until that's released we're using the emacs-tree-sitter package while on Emacs 28.

  (when (< emacs-major-version 29)
    (use-package tree-sitter
    :config
    (global-tree-sitter-mode)
    :ghook
    ('tree-sitter-after-on-hook #'tree-sitter-hl-mode)))

tree-sitter-langs provides tree-sitter support for a bunch of languages.

  (use-package tree-sitter-langs
    :after tree-sitter)

Automatically use the <lang>-ts-mode when it is available

  (use-package treesit-auto
    :config
    (setq treesit-auto-install 'prompt)
    (global-treesit-auto-mode))

eglot

eglot is an alternative to lsp-mode that is builtin with emacs >= 29

  (use-package eglot
    :config
    (fset #'json--log-event #'ignore) ;; Performance boost by not logging every event
    (add-to-list 'eglot-server-programs
                 '((toml-mode toml-ts-mode conf-toml-mode) . ("taplo" "lsp" "stdio")))
    (add-to-list 'eglot-server-programs
                 `((elixir-ts-mode heex-ts-mode) .
                   ,(eglot-alternatives '("lexical" "elixir-ls"))))
    (add-to-list 'eglot-server-programs
                 '(dhall-mode . ("dhall-lsp-server")))
    (add-to-list 'eglot-server-programs
                 '((html-mode mhtml-mode) . ("superhtml" "lsp")))
    (add-to-list 'eglot-stay-out-of 'flymake)
    (setq eglot-autoshutdown t
          eldoc-echo-area-use-multiline-p 0.1)
    :hook
    (eglot-managed-mode . (lambda ()
                            (eglot-inlay-hints-mode 1)
                            (define-key eglot-mode-map (kbd "C-c l a") 'eglot-code-actions)
                            (define-key eglot-mode-map (kbd "C-c l f") 'eglot-format)
                            (define-key eglot-mode-map (kbd "C-c l h") 'eldoc)
                            (define-key eglot-mode-map (kbd "C-c l i") 'eglot-find-implementation)
                            (define-key eglot-mode-map (kbd "C-c l r") 'eglot-rename)
                            (define-key eglot-mode-map (kbd "C-c l t") 'eglot-find-typeDefinition)
                            (define-key eglot-mode-map (kbd "C-c l w d") 'eglot-list-connections)
                            (define-key eglot-mode-map (kbd "C-c l w r") 'eglot-reconnect)
                            (define-key eglot-mode-map (kbd "C-c l w q") 'eglot-shutdown)
                            (define-key eglot-mode-map (kbd "C-c l y") 'eglot-inlay-hints-mode))))

eglot-x adds support for some LSP extensions to eglot

  (use-package eglot-x
    :vc (:fetcher github :repo nemethf/eglot-x)
    :after eglot
    :config
    (eglot-x-setup))

consult-eglot adds an integration between consult and eglot

  (use-package consult-eglot)

Snippets

Snippets are predefined pieces of code that can be inserted and filled in. YASnippet uses syntax inspired by TextMate is the most popular, for good reason.

  (use-package yasnippet
    :init
    (load "yasnippet.el")
    :diminish t
    :general
    (:keymaps 'yas-minor-mode-map
              "<tab>" nil
              "TAB" nil
              "<C-tab>" 'yas-expand)
    :config
    (add-to-list 'yas-snippet-dirs my/snippets-dir)
    (yas-global-mode))

  (use-package yasnippet-snippets)

Languages

JavaScript / TypeScript

Indent 2 spaces

    (setq-default js-indent-level 2
                  typescript-indent-level 2)

js2-mode improves a lot on the builtin js-mode

  (use-package js2-mode
    :after eglot
    :mode
    ((rx ".mjs" eos) . js2-mode)
    ((rx ".jsx?" eos) . js2-jsx-mode)
    :hook
    (js2-mode . eglot-ensure)
    (js2-jsx-mode . eglot-ensure))

Prettier has my preference for formatting JavaScript and TypeScript

    (use-package prettier
      :config
      (setq prettier-enabled-parsers '
            (css html json markdown scss svelte toml typescript vue)))

TypeScript stuff

  (use-package typescript-mode
    :after eglot
    :mode
    ((rx ".tsx?" eos) . typescript-mode)
    :hook (typescript-mode . eglot-ensure))

Prefer local packages from node_modules to global ones

  (use-package add-node-modules-path)

Web mode

web-mode handles HTML/CSS and JavaScript

  (use-package web-mode
    :mode (rx ".svelte" eos)
    :after eglot
    :config
    (setq web-mode-markup-indent-offset 2
          web-mode-css-indent-offset 2
          web-mode-code-indent-offset 2
          web-mode-enable-auto-pairing t
          web-mode-enable-css-colorization t
          web-mode-enable-current-element-highlight t
          web-mode-enable-current-column-highlight t)

    (add-to-list 'web-mode-engines-alist '(("elixir" . (rx ".html.heex" eos))
                                           ("jinja2" . (rx ".jinja2" eos))
                                           ("python" . (rx ".pt" eos)) ; Chameleon templates
                                           ("svelte" . (rx ".svelte" eos))))
    :hook
    ((mhtml-mode css-mode web-mode) . eglot-ensure))

Markdown

markdown-mode adds support for Markdown editing. gfm-mode supports GitHub Flavoured Markdown.

  (use-package markdown-mode
    :after eglot
    :mode
    (((rx "README.md" eos) . gfm-mode)
     ((rx ".md" eos) . markdown-mode)
     ((rx ".markdown" eos) . markdown-mode))
    :init
    (setq markdown-command "multimarkdown")
    :hook
    (markdown-mode . display-fill-column-indicator-mode)
    (markdown-mode . eglot-ensure))

impatient-mode live renders HTML, but it can be made to work with Markdown with a custom filter

  (use-package impatient-mode
    :after markdown-mode
    :config
    (imp-set-user-filter 'markdown-filter))

    (defun markdown-filter (buffer)
      (princ
       (with-temp-buffer
         (let ((tmpname (buffer-name)))
           (set-buffer buffer)
           (set-buffer (markdown tmpname)) ; the function markdown is in `markdown-mode.el'
           (buffer-string)))
       (current-buffer)))

Elixir

Add support for Elixir with elixir-mode. The elixir-format hook sets up the correct formatter configuration when in a projectile project.

  (use-package elixir-mode
    :after eglot
    :hook ((elixir-mode . eglot-ensure))
    :config
    (add-to-list 'auto-mode-alist '((rx ".[hl]eex") . elixir-mode)))
  (use-package elixir-ts-mode
    :after eglot
    :hook ((elixir-ts-mode . eglot-ensure)))

Add a mix minor mode to call mix tasks from emacs.

  (use-package mix
    :hook
    (elixir-mode . mix-minor-mode))

Erlang

  (use-package erlang
    :mode
    ((rx ".P" eos) . erlang-mode)
    ((rx ".E" eos) . erlang-mode)
    ((rx ".S" eos) . erlang-mode)
    :config
    (require 'erlang-start))

Rust

Rust support with rust-mode.

  (use-package rust-mode
    :after eglot
    :hook
    (rust-mode . eglot-ensure)
    (rust-ts-mode . eglot-ensure)
    (before-save . eglot-format-buffer)
    ;; :init
    ;; (setq lsp-rust-analyzer-cargo-watch-command "clippy"
    ;;       lsp-rust-analyzer-server-display-inlay-hints t
    ;;       lsp-rust-analyzer-binding-mode-hints t
    ;;       lsp-rust-analyzer-display-lifetime-elision-hints-enable "always")
    )

Configure rust-analyzer

  (defun eb/ra-eglot-config (server)
    "initializationOptions for rust-analyzer"
    `(:diagnostics (:enable t)
      :imports (:granularity (:enforce :json-false :group "crate")
                :group t :merge
                (:glob t)
                :preferPrelude t
                :prefix "plain")
      :checkOnSave (:enable t
                    :command "clippy"
                    :allTargets t)
      :inlayHints (:bindingModeHints t
                   :chainingHints t
                   :lifetimeElisionHints (:enable "always" :useParameterNames t)
                   :maxLength nil
                   :typeHints (:enable t
                               :hideClosureInitialization :json-false
                               :hideNamedConstructor :json-false))
      :procMacro (:enable t)))

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '(rust-mode . ("rust-analyzer" :initializationOptions eb/ra-eglot-config))))

Add cargo support

  (use-package cargo
    :hook
    (rust-mode . cargo-minor-mode))

Flycheck support for Rust with flycheck-rust

  (use-package flycheck-rust
    :hook
    (flycheck-mode . flycheck-rust-setup))

TOML

Support for TOML files with toml-mode

  (use-package toml-mode
    :mode ((rx ".toml" eos) . conf-toml-mode))

Docker

Add docker support with dockerfile-mode

  (use-package dockerfile-mode
    :after eglot
    :hook (dockerfile-mode . eglot-ensure))

Bitbake / Yocto

    (use-package bitbake-modes
      :straight (:type git :repo "https://bitbucket.org/olanilsson/bitbake-modes.git"))

INI

ini-mode provides highlighting for ini files

  (use-package ini-mode)

JSON

json-mode extends the builtin js-mode with better syntax highlighting for JSON and adds some editing keybindings

  (use-package json-mode
    :after eglot
    :hook (json-mode . eglot-ensure))

CMake

Add cmake-mode

  (use-package cmake-mode)

YAML

Use yaml-mode to handle YAML files

  (use-package yaml-mode
    :after eglot
    :hook (yaml-mode . eglot-ensure))

C/C++

Enable clangd LSP for C and C++

  (use-package cc-mode
    :ensure nil
    :after eglot
    :hook
    ((c-mode c-ts-mode) . eglot-ensure)
    ((c++-mode c++-ts-mode) . eglot-ensure))

Add some flags to clangd

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '((c-mode c-ts-mode c++-mode c++-ts-mode)
                   . ("clangd"
                      "--malloc-trim"
                      "--log=error"
                      "--clang-tidy"
                      "--completion-style=detailed"))))

Add QML mode

      (use-package qml-mode
        :mode (rx ".qml" eos))

Enable and configure auto-insert-mode for Horus projects

  (defconst my/generate-cpp-file-executable
    "~/workspace/horus/development/code-generation/generate-cpp-file.py"
    "Python program to generate a C++ boilerplate")

  (when (file-executable-p my/generate-cpp-file-executable)
    (define-auto-insert
      "\\.[ch]pp\\'"
      (lambda nil (call-process my/generate-cpp-file-executable nil t nil buffer-file-name))))


  (auto-insert-mode)

Meson

meson is a build system designed to be as fast and as user-friendly as possible.

  (use-package meson-mode)

nix

Add nix-mode

  (use-package nix-mode
    :after eglot
    :mode (rx ".nix" eos)
    :hook (nix-mode . eglot-ensure))

Tell nil to use nixfmt for formatting nix files.

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 `(nix-mode . ("nil" :initializationOptions
                               (:formatting (:command ["nixfmt"]))))))

Common Lisp

Common Lisp does not use lsp-mode, but has it's own environment: SLIME or Superior Lisp Interaction Mode for Emacs.

  (use-package slime
    :init
    (setq slime-lisp-implementations
          '((sbcl ("sbcl" "--load ~/.quicklisp/setup.lisp") :coding-system utf-8-unix))
          slime-default-lisp 'sbcl)
    :config
    (slime-setup '(slime-fancy slime-quicklisp slime-asdf))
    :mode
    (((rx ".cl" eos) . lisp-mode))
    :hook
    (lisp-mode . (lambda () (slime-mode t)))
    (inferior-lisp-mode . (lambda () (inferior-slime-mode t))))

SLY is a fork of SLIME, by the same author as eglot, with improved UX

  (use-package sly
    :mode
    (((rx ".cl" eos) . lisp-mode))
    :config
    (setq sly-lisp-implementations
          '((sbcl ("sbcl") :coding-system utf-8-unix))
          sly-default-lisp 'sbcl))

Terraform

terraform-mode is a major mode for Terraform files

  (use-package terraform-mode
    :after eglot
    :hook (terraform-mode . eglot-ensure))

Register terraform-ls with eglot

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '(terraform-mode . ("terraform-ls" "serve"))))

Fish

fish-mode provides a major mode for fish shell scripts

  (use-package fish-mode
    :init
    (setq fish-enable-auto-indent t))

Zig

zig-mode provides a major mode for the zig programming language

  (use-package zig-mode
    :after eglot
    :hook (zig-mode . eglot-ensure))

Yuck

  (use-package yuck-mode)

Racket

  (use-package racket-mode
    :after eglot
    :hook (racket-mode . eglot-ensure))

Ruby

Let ruby-mode handle Ruby files

  (use-package ruby-mode
    :ensure nil
    :after eglot
    :hook (ruby-mode . eglot-ensure))

Go

It's better than nothing.

  (use-package go-mode
    :after eglot
    :hook (go-mode . eglot-ensure))

Cucumber

feature-mode provides support for user stories written in Cucumber/Gherkin format.

  (use-package feature-mode
    :config
    (setq feature-use-docker-compose nil)
    :mode (rx ".feature" eos))

Protobuf

protobuf-mode

  (use-package protobuf-mode)

Just

just-mode provides syntax highlighting for Justfiles, for the command runner just

  (use-package just-mode)

Python

Python

  (use-package python-mode
    :hook
    ((python-mode python-ts-mode) . eglot-ensure))
  (use-package python
    :ensure nil
    :hook (python-base-mode . eglot-ensure)
    :init (setq python-indent-guess-indent-offset nil))

jinja2

  (use-package jinja2-mode)

Haskell

haskell-mode for Haskell files

  (use-package haskell-mode
    :hook
    (((haskell-mode haskell-ts-mode) . eglot-ensure)
     turn-on-haskell-unicode-input-method))

Dhall

Dhall is a programmable configuration language that you can think of as: JSON + functions + types + imports

  (use-package dhall-mode
    :mode (rx ".dhall" eos))

nushell

nushell is a new type of shell that operates on typed data

    (use-package nushell-ts-mode)

Register nushell LSP with eglot

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '(nushell-ts-mode . ("nu" "--lsp"))))

Lua

lua-mode for Lua support.

  (use-package lua-mode)

Device Tree

Highlighting for device tree configuration.

    (use-package dts-mode)

OCaml

Tuareg is the recommended OCaml mode

        (use-package tuareg)

Merlin adds context-sensitive completion

    (use-package merlin
      :init (setq merlin-command "ocamlmerlin")
      :hook (tuareg-mode . merlin-mode))

Gleam

Gleam has an official emacs mode: gleam-ts-mode

    (use-package gleam-ts-mode
      :mode (rx ".gleam" eos)
      :hook (gleam-ts-mode . eglot-ensure))

Configure eglot to use the Gleam LSP server

  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '(gleam-ts-mode . ("gleam" "lsp"))))

Org

Main org setup

org-mode configuration

  (use-package org
    :general
    <<org-binds>>
    :custom
    <<org-customisations>>
    :hook
    <<org-hooks>>
    :config
    <<org-config>>)

Some keybindings for often used functions

  (:prefix my/leader
    "r" 'org-capture
    "a" 'org-agenda
    "s" 'org-store-link
    "L" 'org-store-link-global
    "O" 'org-open-at-point-global)

Customisations

  (org-directory "~/org")
  (org-log-done t)
  (org-indent-indentation-per-level 2)
  (org-startup-indented t)
  (org-log-into-drawer t)
  (org-default-notes-file (expand-file-name "notes.org" org-directory))
  (org-return-follows-link t)
  (org-pretty-entities t)
  (org-hide-emphasis-markers t)
  (org-startup-with-inline-images t)
  (org-startup-with-latex-previews t)
  (org-image-actual-width '(300))
  (add-to-list 'org-export-backends 'md)

Hooks

  ((org-mode . org-indent-mode)
   (org-mode . turn-on-visual-line-mode)
   (org-mode . variable-pitch-mode)
   (org-mode . (lambda () (display-line-numbers-mode -1)))
   (org-mode-indent . (lambda () (diminish 'org-indent-mode)))
   (org-agenda-mode . (lambda () (hl-line-mode 1))))

Configuration

  (add-to-list 'auto-mode-alist '((rx ".org" eos) . org-mode))
  (dolist (face '((org-level-1 . 1.2)
                  (org-level-2 . 1.1)
                  (org-level-3 . 1.05)
                  (org-level-4 . 1.0)
                  (org-level-5 . 1.0)
                  (org-level-6 . 1.0)
                  (org-level-7 . 1.0)
                  (org-level-8 . 1.0)))
    (set-face-attribute (car face) nil :family my/variable-width-font :weight 'medium :height (cdr face)))
  (set-face-attribute 'org-document-title nil :family my/variable-width-font :weight 'bold :height 1.3)
  (set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-formula nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
  (set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
  (set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
  (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

org-capture allows creating and saving quick notes, links and TODOs

  (use-package org-capture
    ;; :straight org
    :ensure org
    :after org
    :config
    (setq org-capture-templates
          '(("t" "Todo" entry (file+headline "gtd.org")
             "* TODO %?\n  %i\n  %a")
            ("n" "Note" entry (file+olp+datetree "notes.org")
             "* %?\nEntered on %U\n  %i\n  %a")
            ("N" "Note (work)" entry (file+olp+datetree "work/notes.org")
             "* %?\nEntered on %U\n  %i\n  %a")
            ("c" "org-protocol-capture" entry (file+headline "inbox.org" "Links")
             "* %^{Title}\n\n[[%:link][%:description]]\n\n %i"
             :immediate-finish t))))

org-datetree allows you to organise captures by date. It is basically a set of headings representing the date, with the first level for the year, the second level for the month and the third for the day.

  (use-package org-datetree
    ;; :straight org
    :ensure org
    :after org)

org-protocol can be used to send data to emacs using org-protocol:// URLs.

  (use-package org-protocol
    ;; :straight org
    :ensure org
    :after org
    :config
    (setq org-protocol-default-template-key "c"))

org-roam

org-roam helps with non-hierarchical note-taking. It uses the zettelkasten method to capture and organise notes and ideas.

  (use-package org-roam
    ;; :after org
    :custom
    (org-roam-directory "~/org-roam")
    (org-roam-completion-everywhere t)
    <<org-roam-templates>>
    :config
    (require 'org-roam-dailies)
    (org-roam-db-autosync-mode)
    :general
    (:prefix my/leader
      :keymaps 'global
      "n f" 'org-roam-node-find
      "n r" 'org-roam-node-random
      "n i" 'org-roam-node-insert
      "n l" 'org-roam-buffer-toggle
      :keymaps 'org-mode-map
      "n o" 'org-id-get-create
      "n t" 'org-roam-tag-add
      "n a" 'org-roam-alias-add
      "n l" 'org-roam-buffer-toggle)
    ("C-M-i" 'completion-at-point
     :keymaps 'org-roam-dailies-map
     "Y" 'org-roam-dailies-capture-yesterday
     "T" 'org-roam-dailies-capture-tomorrow)

    (:prefix my/leader "n d" '(:keymap org-roam-dailies-map)))

Org roam capture templates

  (org-roam-capture-templates '(("d" "default" plain
                                 "%?"
                                 :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+date: %U\n")
                                 :unnarrowed t)
                                ("w" "work note" plain
                                 "%?"
                                 :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :work:\n")
                                 :unnarrowed t)
                                ("p" "project" plain
                                 "* Goals\n\n%?\n\n* Tasks\n\n** TODO Add initial tasks\n\n* Dates\n\n"
                                 :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :project:\n")
                                 :unnarrowed t)))

org-roam-ui provides a frontend to explore and interact with org-roam notes. It can be started with M-x org-roam-ui-mode RET, and is then available on http://127.0.0.1:35901/. Updates are sent real-time through a WebSocket. The settings provided here are also the defaults, set any to nil to disable.

  (use-package org-roam-ui
    :after org-roam
    :config
    (setq org-roam-ui-sync-theme t
          org-roam-ui-follow t
          org-roam-ui-update-on-save t
          org-roam-ui-open-on-start t))

org-chef

org-chef is used to manage recipes and import them from supported websites

  (use-package org-chef
    :after org-capture
    :config
    (add-to-list 'org-capture-templates '("r" "Recipes"))
    (add-to-list 'org-capture-templates '("rr" "Recipe" entry (file "~/org/cookbook.org")
                                          "%(org-chef-get-recipe-from-url)"
                                          :empty-lines 1))
    (add-to-list 'org-capture-templates '("rm" "Manual recipe" entry (file "~/org/cookbook.org")
                                          "* %^{Recipe title: }\n  :PROPERTIES:\n  :source-url:\n  :servings:\n  :prep-time:\n  :cook-time:\n  :ready-in:\n  :END:\n** Ingredients\n  %?\n** Directions\n\n")))

Misc

direnv

envrc adds support for direnv to emacs. envrc works on a per-buffer basis, so when you open multiple projects the environments don't pollute eachother. NOTE: this should be loaded last.

  (use-package envrc
    :init
    (envrc-global-mode)
    :general
    (:prefix my/leader "e" '(:keymap envrc-command-map)))

Writing

Enable flyspell

  (use-package flyspell
    :ensure nil
    :init
    (setq ispell-list-command "--list")
    :hook
    (text-mode . flyspell-mode))

  (use-package flyspell-correct
    :after flyspell
    :general

    (:keymaps 'flyspell-mode-map
              "C-;" 'flyspell-correct-wrapper))

Use ivy for flyspell suggestions. Use M-o to switch to the actions (e.g. saving a word to your dictionary).

  (use-package flyspell-correct-ivy
    :after flyspell-correct)

Load local init file

If local-init.el exists, load it.

  (when (file-readable-p my/local-init-file)
    (load my/local-init-file))

DO NOT ADD ANYTHING BELOW THIS