My Emacs configuration

2023-03-18

Below are fragments from my Emacs configuration file (~/.emacs.d/init.el) using straight.el instead of Emacs builtin package.el. (For old package.el based version see here).

The newest version of this config is available from a github repo.

Bootstrap straight.el

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(setq package-enable-at-startup nil
      straight-use-package-by-default t)
(straight-use-package 'use-package)

Productivity

Key discoverability

(use-package which-key
  :init
  (setq which-key-idle-delay 2.0
        which-key-idle-secondary-delay 1.0)
  :config
  (which-key-mode))

More efficient buffer/file selection

Requires: fzf and ripgrep (rg), and git.

(setq recentf-max-saved-items 100)

(global-set-key "\C-cq" #'bury-buffer)

(use-package prescient
  :config
  (prescient-persist-mode))

(use-package ivy-prescient
  :after counsel
  :config
  (ivy-prescient-mode))

(use-package counsel
  :demand
  :bind (("C-c a" . swiper-all)
         ("C-c e" . counsel-flycheck)
         ("C-c f" . counsel-fzf)
         ("C-c g" . counsel-git)
         ("C-c i" . counsel-imenu)
         ("C-c j" . counsel-git-grep)
         ("C-c L" . counsel-locate)
         ("C-c o" . counsel-outline)
         ("C-c r" . counsel-rg)
         ("C-c R" . counsel-register)
         ("C-c T" . counsel-load-theme)
         ("C-S-s" . swiper)
         ([remap dired] . counsel-dired))
  :init
  (setq ivy-use-virtual-buffers t)
  :config
  (ivy-mode 1)
  (counsel-mode 1))

(use-package ivy-rich
  :after ivy
  :config
  (ivy-rich-mode 1))

(use-package helpful
  :init
  (setq counsel-describe-function-function #'helpful-callable)
  (setq counsel-describe-variable-function #'helpful-variable))

File explorer sidebar

Install fonts with M-x all-the-icons-install-fonts.

(use-package dired-sidebar
  :bind ("C-c s" . dired-sidebar-toggle-sidebar))

(use-package all-the-icons
  :defer)

(use-package all-the-icons-dired
  :hook (dired-mode . all-the-icons-dired-mode))

(use-package all-the-icons-ivy-rich
  :after ivy-rich
  :config (all-the-icons-ivy-rich-mode 1))

(use-package treemacs
  :bind ("C-c S" . treemacs))

Window selection enhancements

(use-package windmove
  :bind (("H-w j" . windmove-down)
         ("H-w k" . windmove-up)
         ("H-w h" . windmove-left)
         ("H-w l" . windmove-right)))

(use-package windswap
  :bind (("H-w J" . windswap-down)
         ("H-w K" . windswap-up)
         ("H-w H" . windswap-left)
         ("H-w L" . windswap-right)))

In buffer movement enhancements

Search

(use-package ctrlf
  :config
  (ctrlf-mode))

Improved in buffer search

(use-package ace-jump-mode
  :bind ("C-." . ace-jump-mode))

Go to last change in the current buffer

(use-package goto-chg
  :bind ("C-c G" . goto-last-change))

Editing enhancements

Context aware insertion of pairs of parenthesis

(use-package smartparens
  :hook ((prog-mode . smartparens-mode)
         (emacs-lisp-mode . smartparens-strict-mode))
  :init
  (setq sp-base-key-bindings 'sp)
  :config
  (define-key smartparens-mode-map [M-backspace] #'backward-kill-word)
  (define-key smartparens-mode-map [M-S-backspace] #'sp-backward-unwrap-sexp)
  (require 'smartparens-config))

Edit with multiple cursors

(use-package multiple-cursors
  :bind (("C-c n" . mc/mark-next-like-this)
         ("C-c p" . mc/mark-previous-like-this)))

Fix trailing spaces but only in modified lines

(use-package ws-butler
  :hook (prog-mode . ws-butler-mode))

Switching buffers

Bind keys from H-r a to H-r z to switch to buffers from a register from a to z.

(defalias 'pr #'point-to-register)

(defun my-switch-to-register ()
  "Switch to buffer given by key binding last character."
  (interactive)
  (let* ((v (this-command-keys-vector))
         (c (aref v (1- (length v))))
         (r (get-register c)))
    (if (and (markerp r) (marker-buffer r))
        (switch-to-buffer (marker-buffer r))
      (jump-to-register c))))

(setq my-switch-to-register-map (make-sparse-keymap))

(dolist (character (number-sequence ?a ?z))
  (define-key my-switch-to-register-map
    (char-to-string character) #'my-switch-to-register))

(global-set-key (kbd "H-r") my-switch-to-register-map)

Switching tabs

(dolist (i (number-sequence 1 9))
  (global-set-key (format "\C-c%d" i) `(lambda () (interactive) (tab-select ,i))))

(tab-bar-mode 1)

Yasnippet and abbrev mode

(setq-default abbrev-mode 1)

(use-package yasnippet
  :defer 2
  :config
  (yas-global-mode 1))

(use-package yasnippet-snippets
  :defer)

(use-package ivy-yasnippet
  :bind ("C-c y" . ivy-yasnippet))

Programming modes

company and lsp-mode

For modes using company for tab completion add

(use-package company
  :bind (:map prog-mode-map
         ("C-i" . company-indent-or-complete-common)
         ("C-M-i" . counsel-company))
  :hook (emacs-lisp-mode . company-mode))

(use-package company-prescient
  :after company
  :config
  (company-prescient-mode))

For modes that also use Language Server Protocol from lsp-mode add

(use-package lsp-mode
  :hook ((c-mode c++-mode d-mode elm-mode go-mode js-mode kotlin-mode python-mode
          typescript-mode vala-mode web-mode)
         . lsp)
  :init
  (setq lsp-keymap-prefix "H-l"
        lsp-rust-analyzer-proc-macro-enable t)
  :config
  (lsp-enable-which-key-integration t))

(use-package lsp-ui
  :init
  (setq lsp-ui-doc-position 'at-point
        lsp-ui-doc-show-with-mouse nil
        lsp-ui-sideline-show-code-actions t)
  :bind (("C-c A" . lsp-execute-code-action)
         ("C-c d" . lsp-ui-doc-show)
         ("C-c I" . lsp-ui-imenu)))

(use-package flycheck
  :defer)

Compilation

(global-set-key "\C-ck" #'compile)

C and C++

Requires: company and lsp-mode, clangd.

(use-package cc-mode
  :straight nil
  :bind (:map c-mode-map
         ("C-i" . company-indent-or-complete-common)
         :map c++-mode-map
         ("C-i" . company-indent-or-complete-common))
  :init
  (setq-default c-basic-offset 8))

D

Requires: company and lsp-mode, dmd, dub, dcd, and dtools (for rdmd) to build serve-d (but first check for newer version) with

$ dub fetch serve-d@0.7.4
$ dub run serve-d --build=release
$ ln -s ~/.dub/packages/serve-d-0.7.4/serve-d/serve-d ~/.local/bin/
(use-package d-mode
  :bind (:map d-mode-map
         ("C-i" . company-indent-or-complete-common)))

Dart

Requires: company and lsp-mode, flutter in PATH, flutter doctor should be OK, and M-x lsp-dart-dap-setup.

(use-package dart-mode)

(use-package lsp-dart
  :hook (dart-mode . lsp)
  :bind (:map lsp-mode
         ("C-M-x" . lsp-dart-dap-flutter-hot-reload)))

Elm

Requires: company and lsp-mode.

(use-package elm-mode
  :hook (elm-mode . elm-indent-mode))

Emacs lisp

Requires: company and lsp-mode (actually just company) and smartparens.

Go

Requires: company and lsp-mode, gopls.

(defun my-go-before-save ()
  "Format buffer and organize imports in Go mode."
  (when (eq major-mode 'go-mode)
    (lsp-organize-imports)
    (lsp-format-buffer)))

(use-package go-mode
  :hook (before-save . my-go-before-save))

JavaScript

Requires: company and lsp-mode ts-ls (proposed by lsp-mode when missing).

(use-package js
  :straight nil
  :bind (:map js-mode-map
         ([remap js-find-symbol] . xref-find-definitions))
  :init
  (setq js-indent-level 4))

Kotlin

Requires: kotlin-language-server.

(use-package kotlin-mode)

Lisp

(use-package sly
  :defer
  :config
  (setq inferior-lisp-program "sbcl"
        sly-mrepl-pop-sylvester nil)
  :custom-face
  (sly-mrepl-output-face ((t (:foreground "sienna")))))

(use-package paren-face
  :defer)

(use-package paredit
  :defer)

(use-package highlight-parentheses
  :defer)

(defun my-lisp-mode-hook-fn ()
  (smartparens-mode 0)
  (paredit-mode 1)
  (paren-face-mode 1)
  (highlight-parentheses-mode 1)
  (company-mode 1))

(add-hook 'lisp-mode-hook 'my-lisp-mode-hook-fn)
(add-hook 'emacs-lisp-mode-hook 'my-lisp-mode-hook-fn)

Meson build system

(use-package meson-mode
  :defer
  :init
  (setq meson-indent-basic 4))

Python

Requires: company and lsp-mode, pylsp.

Rust

Requires: company and lsp-mode, rust-src from Rustup and rust-analyzer.

(use-package rustic
  :defer)

TypeScript

Requires: company and lsp-mode, web-mode settings (for tsx), ts-ls (proposed by lsp-mode when missing), tsconfig.json.

(use-package typescript-mode
  :defer)

Vala

Requires: company and lsp-mode, vala-language-server.

(use-package vala-mode
  :defer)

Web mode

Requires: company and lsp-mode.

(use-package web-mode
  :mode "\\.\\([jt]sx\\)\\'")

Other modes

(use-package ansible
  :straight (:nonrecursive t))

(use-package devdocs
  :bind ("C-c D" . devdocs-lookup))

(use-package diff-hl
  :defer)

(use-package htmlize
  :defer)

(use-package rainbow-mode
  :hook css-mode)

(use-package restclient
  :defer)

(use-package vterm
  :defer
  :init
  (dolist (i (number-sequence 0 9))
    (global-set-key (kbd (format "H-%d" i)) `(lambda () (interactive) (vterm ,i)))))

(use-package yaml-mode
  :defer)

Git

(use-package magit
  :bind (("C-x g" . magit-status)
         ("C-x M-g" . magit-dispatch))
  :init
  (setq project-switch-commands nil)) ; avoid magit error on C-n/C-p

(use-package git-messenger
  :bind ("C-x G" . git-messenger:popup-message)
  :config
  (setq git-messenger:show-detail t
        git-messenger:use-magit-popup t))

(use-package git-timemachine
  :bind ("C-c t" . git-timemachine))

Appearance

Minimalistic look

(setq inhibit-startup-screen t)
(set-scroll-bar-mode 'right)
(menu-bar-mode 0)
(tool-bar-mode 0)
(scroll-bar-mode 0)

Mode line

(use-package smart-mode-line
  :config
  (setq sml/no-confirm-load-theme t
        sml/shorten-directory t
        sml/shorten-modes t
        sml/name-width 50
        sml/mode-width 'full
        sml/theme 'respectful)
  (sml/setup))

Switching themes

(use-package base16-theme
  :defer)

(use-package faff-theme
  :defer)

(setq my-dark-theme 'base16-espresso
      my-light-theme 'base16-mexico-light)

(defun my-select-theme (theme)
  (mapc #'disable-theme custom-enabled-themes)
  (load-theme (cond
               ((eq theme 'dark) my-dark-theme)
               ((eq theme 'light) my-light-theme)
               (t theme))
              t)
  (sml/setup))

(defun my-select-theme-if-none-selected (frame)
  (if (and (eq 'x (window-system frame))
           (null (seq-filter (lambda (theme)
                               (not (string-prefix-p "smart-mode-line-"
                                                     (symbol-name theme))))
                             custom-enabled-themes)))
      (my-select-theme 'dark)))

(my-select-theme-if-none-selected nil)
(add-to-list 'after-make-frame-functions #'my-select-theme-if-none-selected)

(defun my-toggle-theme ()
  "Toggle between dark and light themes."
  (interactive)
  (my-select-theme (if (custom-theme-enabled-p my-dark-theme)
                       my-light-theme
                     my-dark-theme)))

(global-set-key (kbd "C-S-<f6>") #'my-toggle-theme)