Literate .emacs.d

Kris Molendyke

Literate programming is a methodology that combines a programming language with a documentation language, thereby making programs more robust, more portable, more easily maintained, and arguably more fun to write than programs that are written only in a high-level language. The main idea is to treat a program as a piece of literature, addressed to human beings rather than to a computer. The program is also viewed as a hypertext document, rather like the World Wide Web. (Indeed, I used the word WEB for this purpose long before CERN grabbed it!)

All configuration described here is performed in an after-init-hook function.

Table of Contents

Build Emacs

I primarily use Emacs locally on Apple hardware. Most, if not all, of the remote editing that I do is done via tramp. I keep an eye on the log at the official Emacs git repository. When something interesting lands there I will build a fresh Emacs installation via Homebrew and try it out.

brew info emacs

I install Emacs with the following options. The standard Homebrew installation has lost a lot of MacOS customization flags over the years so I've switched to the Emacs Plus formula instead. First, tap that keg:

brew tap d12frosted/emacs-plus

And then install:

brew install emacs-plus@29

Emacs Source Directory

source-directory is helpful to have set properly when exploring built-in C functions via find-function.

Finding the source code directory via Homebrew can be done with the following command:

echo $(brew --cache)/emacs*
/Library/Caches/Homebrew/emacs--git

Note: source-directory must be set during Emacs initialization time to persist. See init.el.

How this works

The init.el file bootstraps the entire process. It is found by Emacs automatically if it is located in one of several well-defined locations.

When Emacs is started, it normally tries to load a Lisp program from an "initialization file", or "init file" for short. This file, if it exists, specifies how to initialize Emacs for you. Emacs looks for your init file using the filenames `~/.emacs', `~/.emacs.el', or `~/.emacs.d/init.el'; you can choose to use any one of these three names (*note Find Init::). Here, `~/' stands for your home directory.

Once it's found, init.el sets "global" variables and defines several functions to be executed during initialization and one that should be executed after initialization has completed. k20e/after-init-hook is a hook run after initialization. The most interesting bit of this function is the call to org-babel-load-file:

(org-babel-load-file FILE &optional COMPILE)

Load Emacs Lisp source code blocks in the Org-mode FILE. This function exports the source code using `org-babel-tangle' and then loads the resulting file using `load-file'. With prefix arg (noninteractively: 2nd arg) COMPILE the tangled Emacs Lisp file to byte-code before it is loaded.

This function invocation exports all org-mode source blocks in any org-mode files in the .emacs.d directory to a single Emacs Lisp file and then instructs Emacs to load that file. custom.org is the biggest of those org-mode files and … this is that file!

HTML Export post-commit Hook

This git post-commit hook is located at .git/hooks/post-commit and automatically generates the HTML content of this GitHub Pages site.

#!/bin/bash

repo_root=$(git rev-parse --show-toplevel)
htmlize_dir=$(dirname "$(find "${repo_root}" -type f -name htmlize.el)")

emacs --batch \
      --directory "${htmlize_dir}" \
      --directory "$HOME/.emacs.d/site-lisp/org-mode/lisp" \
      --load "$HOME/.emacs.d/elisp/k20e-org-html-export.el" \
      --visit "$HOME/.emacs.d/custom.org" \
      --execute '(org-html-export-to-html)' > /dev/null 2>&1
git checkout gh-pages
mv custom.html index.html
git add index.html
git commit --message "post-commit hook regeneration."
git checkout master

All of the interesting customization of the exported document is performed in elisp/k20e-org-html-export.el.

use-package

Before use-package I had 70 packages installed and emacs-init-time was "8.678111 seconds".

(eval-when-compile (require 'use-package))

(use-package package
  :config
  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t))

Performance Tuning

Mucking with these solely based on output from lsp-doctor and its recommendations since I use this all day writing Go programs.

GC

(eval-when-compile
  (let ((mib (expt 2 20)))
    (setq gc-cons-threshold (* 500 mib))))

Process Output

(eval-when-compile
  (let ((mib (expt 2 20)))
    (setq read-process-output-max (* 1 mib))))

Global GNU Emacs Key Bindings

These global key bindings override built-in functions only. Package-specific or custom function defunition key bindings are made in their own dedicated sections where other specific settings are made.

Unset

OS X annoyance – C-M-d is a "hot key" bound to dictionary lookup and masks the key binding in Emacs. Disabling it can currently only be done by editing a default and restarting.

defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys \
         -dict-add 70 '<dict><key>enabled</key><false/></dict>'
(global-unset-key (kbd "<f1> h"))
(global-unset-key (kbd "<f11>"))
(global-unset-key (kbd "C-h"))
(global-unset-key (kbd "C-q"))
(global-unset-key (kbd "M-`"))
(global-unset-key (kbd "M-c"))
(global-unset-key (kbd "M-h"))
(global-unset-key (kbd "M-u"))

Set

(global-set-key (kbd "<f1> F") 'find-function)
(global-set-key (kbd "<f1> V") 'find-variable)
(global-set-key (kbd "<f6>") 'k20e/font-toggle-proportional)
(global-set-key (kbd "<f7>") 'previous-error) ;; ◀◀
(global-set-key (kbd "<f9>") 'next-error) ;; ▶▶
(global-set-key (kbd "C-M-;") 'comment-line)
(global-set-key (kbd "C-S-h") 'kill-whole-line)
(global-set-key (kbd "C-c DEL") 'join-line)
(global-set-key (kbd "C-h") 'delete-backward-char)
(global-set-key (kbd "C-j") 'join-line)
(global-set-key (kbd "C-x C-t") 'transpose-lines)
(global-set-key (kbd "H-h H-f") 'find-function)
(global-set-key (kbd "H-h H-v") 'find-variable)
(global-set-key (kbd "H-t") 'toggle-frame-fullscreen)
(global-set-key (kbd "M-+") 'text-scale-adjust)
(global-set-key (kbd "M-.") 'imenu)
(global-set-key (kbd "M-/") 'hippie-expand)
(global-set-key (kbd "M-`") 'other-window)
(global-set-key (kbd "M-h") 'backward-kill-word)
(global-set-key (kbd "M-t") 'transpose-words)

k20e Defaults

Apropos

Sort by relevancy.

(setq-default apropos-sort-by-scores t)

Backup Files

Back up files to a single location.

(defvar k20e/backup-dir (expand-file-name "backup" user-emacs-directory)
  "A single directory for storing backup files within.")

(unless (file-exists-p k20e/backup-dir) (make-directory k20e/backup-dir))

(setq backup-by-copying t
      backup-directory-alist `(("." . ,k20e/backup-dir))
      delete-old-versions t
      version-control t)

Enabled Commands

Commands disabled by default prompt at first use. Enabling commands disables the prompt.

(defvar k20e/enabled-commands
  '(downcase-region
    upcase-region
    narrow-to-region
    narrow-to-page
    scroll-left
    scroll-right)
  "Normally disabled commands.")

(defun k20e/enable-commands ()
  "Enabled normally disabled commands."
  (dolist (command k20e/enabled-commands)
    (put command 'disabled nil)))

(k20e/enable-commands)

Inferior Shell

Defaulting to sh seems to work well.

(setq shell-file-name "/bin/sh")

TODO Defaults for Review

This is a bunch of stuff that I just dumped here and need to go through yet.

Show the active region and delete it when selected if a character is inserted.

(transient-mark-mode t)
(delete-selection-mode 1)

"Electric" indentation is generally what I consider to be sensible.

(electric-indent-mode)

Cycle through the mark ring faster.

(setq set-mark-command-repeat-pop t)

Splitting windows horizontally makes more sense on all of the wide screen monitors I work on.

(setq split-width-threshold 81)
;; What's going on here?
(setq echo-keystrokes 0.1)

;; Automatically reload buffers when files change on disk.
(global-auto-revert-mode 1)

;; y is the new yes.  n is the new no.
(defalias 'yes-or-no-p 'y-or-n-p)

Stuff in review from https://www.masteringemacs.org/article/whats-new-in-emacs-28-1.

(setq-default
 completions-detailed t
 describe-bindings-outline t
 mode-line-compact t
 next-error-message-highlight t
 frame-title-format '(multiple-frames "%b" ("%b\t%f")))

k20e Custom Functions

I have found these to be useful enough to keep around permanently.

Editing

(defun k20e/mark-current-line (arg)
  "Mark the current line.
If the mark is already set simply move the point forward a single
line.  If it is not set, set it at the beginning of the current
line and then move the point forward a single line."
  (interactive "p")
  (unless mark-active
    (beginning-of-line)
    (set-mark (point)))
  (forward-line arg))

(defun k20e/open-line-below (arg)
  "Insert a new line below the current line."
  (interactive "p")
  (end-of-line)
  (newline arg)
  (indent-for-tab-command))

(defun k20e/open-line-above (arg)
  "Insert a new line above the current line."
  (interactive "p")
  (beginning-of-line)
  (newline arg)
  (forward-line (- 0 arg))
  (indent-for-tab-command))

;; Inspired by http://whattheemacsd.com/key-bindings.el-01.html
(use-package display-line-numbers :ensure t)

(defun k20e/goto-line ()
  "Show line numbers and prompt for a line number to go to.
Restore previous state of displaying line numbers."
  (interactive)
  (if display-line-numbers-mode
    (call-interactively 'goto-line)
    (unwind-protect
        (progn
          (setq display-line-numbers t)
          (call-interactively 'goto-line))
      (setq display-line-numbers nil))))

This one is stolen from magnars:

(defun k20e/eval-and-replace ()
  "Replace the preceding sexp with its value."
  (interactive)
  (backward-kill-sexp)
  (condition-case nil
      (prin1 (eval (read (current-kill 0)))
             (current-buffer))
    (error (message "Invalid expression")
           (insert (current-kill 0)))))

Bind editing functions:

(global-set-key (kbd "M-l") 'k20e/mark-current-line)
(global-set-key (kbd "<M-return>") 'k20e/open-line-below)
(global-set-key (kbd "<M-S-return>") 'k20e/open-line-above)
(global-set-key [remap goto-line] 'k20e/goto-line)

Buffers

(defun k20e/display-buffer-file-name ()
  "Message the full path to the currently visited file."
  (interactive)
  (message "%s" (buffer-file-name)))

Toggle Source/Test Buffer

If this gets any smarter it should be refactored into its own package.

(defun k20e/test-buffer-p ()
  "Is the current buffer a test buffer?
This function naïvely assumes that the file name suffix '_test'
is indicative of a test file."
  (string-suffix-p
   "_test"
   (file-name-sans-extension (buffer-file-name))))

(defun k20e/switch-to-test-buffer ()
  "Switch to the test buffer associated with the current source buffer.
FIX: when >1 buffer w/ same name this is wrong because the buffer
name is prepended w/ dir name or whatever"
  (let ((d (file-name-directory (buffer-file-name)))
        (f (format "%s_test.%s"
                    (file-name-sans-extension (buffer-name))
                    (file-name-extension (buffer-file-name)))))
    (find-file (expand-file-name f d))))

(defun k20e/switch-to-source-buffer ()
  "Switch to the source buffer associated with the current test buffer."
  (let ((e (file-name-extension (buffer-file-name)))
        (f (car (split-string (file-name-sans-extension (buffer-file-name))
                              "_test"))))
    (find-file (format "%s.%s" f e))))

(defun k20e/toggle-test-buffer ()
  "Toggle between a source and test buffer.
This function naïvely assumes that the file name suffix '_test'
is indicative of a test file.  Therefore it should only be useful
in major modes where that convention is expected."
  (interactive)
  (if (k20e/test-buffer-p)
      (k20e/switch-to-source-buffer)
    (k20e/switch-to-test-buffer)))

Widescreen

When working on a widescreen monitor it can be useful to have windows arranged a bit differently than they would on smaller monitors. In particular, a function like fit-window-to-buffer which adjusts the window's width is helpful.

(defun k20e/get-longest-line-length ()
  "Get the length of the longest line in the selected window."
  (save-excursion
    (goto-char (point-min))
    (let ((max-length 0)
          (last-line (count-lines (point-min) (point-max))))
      (while (<= (line-number-at-pos) last-line)
        (setq max-length (max max-length (- (line-end-position) (line-beginning-position))))
        (forward-line))
      (1+ max-length))))

(defun k20e/fit-window-to-buffer-horizontally ()
  "Fit the selected window to the width of its longest line.
Return the window width delta."
  (interactive)
  (let* ((current-width (window-width))
         (longest-line (k20e/get-longest-line-length))
         (delta (* -1 (- current-width longest-line))))
    (if (zerop (window-resizable (selected-window) delta t)) nil
      (window-resize (selected-window) delta t))
    delta))

(global-set-key (kbd "C-x w") 'k20e/fit-window-to-buffer-horizontally)

Windows

(use-package windmove :ensure t)

(use-package emacs
  :init
  (defun k20e/delete-window-balance ()
    "Balance windows after deleting."
    (interactive)
    (delete-window)
    (balance-windows-area))

  (defun k20e/split-window-right-switch-buffer (&optional arg)
    "Split the window to the right, balance, and switch to buffer.
Optional argument ARG Prefix argument will prompt for buffer
name."
    (interactive "P")
    (split-window-right)
    (balance-windows-area)
    (windmove-right)
    (if arg
        (switch-to-buffer (read-buffer "Buffer to display to right: "))
      (switch-to-buffer nil)))
  :bind (("C-x 3" . k20e/split-window-right-switch-buffer)
         ("C-x 0" . k20e/delete-window-balance)))

Networking

(use-package net-utils :ensure t)
(use-package tramp
  :ensure t
  :commands tramp-parse-shosts)

(defun k20e/known-hosts ()
  "Get a host name from ~./ssh/known_hosts file."
  (completing-read "host: "
                   (let ((value))
                     (dolist (elt (tramp-parse-shosts "~/.ssh/known_hosts") value)
                       (if elt (setq value (cons (cadr elt) value)))))))

(defun k20e/host-ip ()
  "Insert the current IP of a host using `dns-lookup-program'.
Similar to but simpler than `dns-lookup-host'."
  (interactive)
  (let ((host (k20e/known-hosts)))
    (insert (car (last (split-string (shell-command-to-string
                                      (concat dns-lookup-program " " host))))))))

Lunar 🌙

(require 'calendar)
(require 'lunar)

(defun k20e/full-moons-info ()
  "Get a list of upcoming full moons info beginning with the current month.
See `lunar-phase-list' and `lunar-phase-name'."
  (let* ((current-date (calendar-current-date))
         (current-month (car current-date))
         (current-year (car (last current-date)))
         (full-moon-phase-index 2)
         (k20e/full-moons-info '()))
    (dolist (phase (lunar-phase-list current-month current-year))
      (if (= (car (last phase)) full-moon-phase-index)
          (setq k20e/full-moons-info (cons phase k20e/full-moons-info))))
    (reverse k20e/full-moons-info)))

(defun k20e/full-moons ()
  "Display upcoming full moons beginning with the current month."
  (interactive)
  (with-output-to-temp-buffer "*full-moons*"
    (princ
     (mapconcat
      #'(lambda (x)
          (format "%s %s" (calendar-date-string (car x)) (car (cdr x))))
      (k20e/full-moons-info)
      "\n"))))

auto-fill

When to turn on auto-fill and set fill-column to a reasonable value. This would probably be better dealt with by a data structure that maps mode hooks to fill-column values.

(use-package emacs
  :config
  (setq-default fill-column 118))

auto-package-update

(use-package auto-package-update
  :ensure t
  :config
  (setq-default auto-package-update-delete-old-versions t
                auto-package-update-interval 7
                auto-package-update-prompt-before-update t
                auto-package-update-show-preview t))

auto-save

Disable auto-save.

(use-package emacs
  :config
  (setq auto-save-default nil
        auto-save-timeout 0))

avy

(use-package avy
  :ensure t
  :bind (("s-g" . avy-goto-char-timer))
  :config
  (setq-default
   avy-keys '(?a ?o ?e ?u ?i ?d ?h ?t ?n)
   avy-all-windows nil
   avy-style 'at-full))

buffer-move

Move the current buffer up/down/left/right easily.

(use-package buffer-move
  :ensure t
  :bind (("M-S-<up>" . buf-move-up)
     ("M-S-<down>" . buf-move-down)
     ("M-S-<left>" . buf-move-left)
     ("M-S-<right>" . buf-move-right)))

C

(use-package cc-mode
  :after (flycheck)
  :custom
  (flycheck-gcc-pedantic-errors t)
  :hook ((c-mode . flycheck-mode)))

clock-face

This is a ridiculous package that I wrote to insert a Unicode clock face character for the nearest current half-hour. 🕙

(use-package clock-face
  :ensure nil
  :load-path "site-lisp/clock-face.el")

compilation-mode

(use-package compile
  :custom-face (compilation-error ((t (:foreground "tomato1")))))

Functions to execute after compilation has finished:

(require 'hl-line)
(require 'subr-x)

(defun k20e/compilation-finish-function-delay-delete (buf result)
  "Delete and bury BUF after short delay.
Do so only if compilation is successful."
  (if (string= (string-trim result) "finished")
      (run-with-timer
       1.0 nil
       (lambda (buf)
         (with-current-buffer buf
           (delete-window)
           (bury-buffer)))
       buf)))

(defun k20e/compilation-finish-function-select-window (buf result)
  "Switch to the compilation buffer BUF.
When compilation completes, regardless of result."
  (let ((win (get-buffer-window buf)))
    (select-window (get-buffer-window buf))
    (goto-char (point-max))
    (forward-line -1)
    (hl-line-mode)))

dired

(use-package dired
  :custom-face (dired-flagged ((t (:foreground "tomato1" :strike-through t))))
  :config
  (use-package autorevert
    :config
    (setq auto-revert-verbose nil))
  (use-package dired-x
    :config
    (load "dired-x.el"))
  :hook ((dired-mode . auto-revert-mode)))

electric-pair-mode

(use-package elec-pair
  :hook ((prog-mode . electric-pair-mode)))

emacs-lisp-mode

(use-package elisp-mode
  :config
  (defun k20e/ert ()
    "Run all the tests in the universe!"
    (interactive)
    (ert t))
  :bind (:map emacs-lisp-mode-map ("H-t" . k20e/ert))
  :hook ((emacs-lisp-mode . eldoc-mode)))

expand-region

(use-package expand-region
  :ensure t
  :bind (("C-M-SPC" . er/expand-region)))

find-file-in-project

(use-package find-file-in-project
  :ensure t
  :bind (("C-x o" . find-file-in-project))
  :config
  (setq-default ffip-limit 8192
        ffip-find-options "-not -regex \".*/build.*\"" ; TODO ignore .tox
        ffip-full-paths t
        ffip-patterns (list "*.el"
                "*.html"
                "*.js"
                "*.json"
                "*.go"
                "*.md"
                "*.org"
                "*.py"
                "*.sh"
                "*.txt"
                "*.yaml"
                "*.yml"
                "Dockerfile"
                "Makefile")
        ffip-prune-patterns (list ".git" "build")))

flycheck

(use-package flycheck
  :ensure t
  :config
  (setq-default flycheck-pylintrc "pylintrc"
                flycheck-check-syntax-automatically '(mode-enabled save)))

flyspell

Setup ispell to use aspell, then setup flyspell itself. It requires ispell.

(use-package flyspell
  :ensure t
  :init
  (setq-default ispell-program-name "aspell"
                ispell-extra-args (list "--sug-mode=ultra"))
  :config
  (setq flyspell-issue-message-flag nil
        flyspell-issue-welcome-flag nil)
  :hook ((text-mode . flyspell-mode)))

Install aspell

Install aspell via Homebrew:

brew install aspell --with-lang-en

Fonts

(global-prettify-symbols-mode 1)

(defvar k20e/font-list '(("PragmataPro-Mono-Liga" . 16)
                         ("PragmataPro-Liga" . 16))
  "Ordered list of preferred fonts and sizes.")

(defun k20e/font--set (font-alist)
  "Set the font family and size to the given font alist of the
format (family . point)."
  (let ((font (replace-regexp-in-string "-" " " (car font-alist)))
        (height (* 10 (cdr font-alist))))
    (set-frame-font font)
    (set-face-attribute 'default nil :height height)))

(defun k20e/font-set-from-list (l)
  "Set the font to first available font alist in the given list."
  (if (null l) nil
    (k20e/font--set (car l))
    (if (string= (replace-regexp-in-string "-" " "(caar l))
                 (face-attribute 'default :family (selected-frame)))
        (caar l)
      (k20e/font-set-from-list (cdr l)))))

(defun k20e/font-set ()
  "Set a font from the `k20e/font-list'."
  (interactive)
  (let ((ignore-case completion-ignore-case))
    (unwind-protect
        (progn
          (setq completion-ignore-case t)
          (let ((font (completing-read "Font: " k20e/font-list)))
            (k20e/font--set (assoc font k20e/font-list))))
      (setq completion-ignore-case ignore-case))))

(k20e/font-set-from-list k20e/font-list)

Unicode

Symbola is a nice font for displaying Unicode characters 🍺👍.

(when (member "Symbola" (font-family-list))
  (set-fontset-font t 'unicode "Noto Color Emoji" nil 'prepend))

font-awesome

This is a naïve package that I wrote to help insert Font Awesome icons into buffers.

(use-package font-awesome
  :ensure nil
  :load-path "site-lisp/font-awesome.el")

geiser

(use-package geiser
  :ensure t
  :config
  (setq-default geiser-active-implementations '(racket chicken)
                geiser-default-implementation 'racket))

git

A simple function to insert starter .gitignore file contents from the github/gitignore repository.

(require 'url)

(defun k20e/gh--gitignore-url (language)
  "Get GitHub .gitignore URL for LANGUAGE."
  (format "https://raw.githubusercontent.com/github/gitignore/master/%s.gitignore"
          (capitalize language)))

(defun k20e/gh--gitignore-get-region (response-buffer)
  "Get GitHub .gitignore response body bounds.
Argument RESPONSE-BUFFER HTTP GET response."
  (with-current-buffer response-buffer
    (goto-char (point-min))
    (let ((start (1+ (search-forward-regexp "^$")))
          (end (point-max)))
      (list start end))))

(defun k20e/gh-gitignore-insert (language)
  "Insert Github .gitignore for LANGUAGE."
  (interactive "sLanguage: ")
  (let* ((response-buffer (url-retrieve-synchronously
                           (k20e/gh--gitignore-url language) t))
         (gitignore-region (k20e/gh--gitignore-get-region response-buffer)))
    (insert-buffer-substring-no-properties
     response-buffer (car gitignore-region) (cadr gitignore-region))))

go-mode

Install commands:

pushd "${TMPDIR}" && \
    go install golang.org/x/tools/gopls@latest; \
    go install honnef.co/go/tools/cmd/staticcheck@latest; \
    popd

Switched from staticcheck to golangci-lint but its install doesn't recommend go get -u.

(use-package company
  :ensure t
  :bind
  (:map company-active-map
        ("C-n" . 'company-select-next)
        ("C-p" . 'company-select-previous)
        ("C-h" . 'delete-backward-char))
  :config
  (setq company-idle-delay nil
        company-show-quick-access t
        company-tooltip-align-annotations t))

(use-package dash
  :ensure t)

(use-package flycheck
  :ensure t)

(use-package go-mode
  :ensure t
  :bind
  (:map go-mode-map
        ("C-c C-t" . 'k20e/toggle-test-buffer)
        ("C-." . 'company-indent-or-complete-common))
  :custom-face (button ((t (:box nil))))
  :preface
  (defun k20e/go-mode-hook ()
    (message "k20e/go-mode-hook"))
  :hook
  ((before-save . (lambda () (when (eq major-mode 'go-mode)
                          (lsp-format-buffer)
                          (lsp-organize-imports))))
   (go-mode . lsp-deferred)
   (go-mode . k20e/go-mode-hook)
   (go-mode . (lambda ()
                (let ((gopath (string-trim (shell-command-to-string (string-join `(,(executable-find "go") "env" "GOPATH") " "))))
                      (goroot (string-trim (shell-command-to-string (string-join `(,(executable-find "go") "env" "GOROOT") " ")))))
                  (setenv "GOPATH" gopath)
                  (setenv "GOROOT" goroot))
                (setq-local compile-command "go run main.go"))))
  :config
  (setq-default indent-tabs-mode t
                tab-width 4)

  ;; lsp (currently gopls)

  ;; https://github.com/golang/tools/blob/master/gopls/doc/emacs.md#configuring-lsp-mode
  ;; Officially supported settings get first class vars, experimental
  ;; settings are "custom" strings set via
  ;; lsp-register-custom-settings.

  ;; Officially supported settings
  ;; https://github.com/golang/tools/blob/master/gopls/doc/settings.md#officially-supported
  (setq-default lsp-go-directory-filters ["-**/testdata"]
                lsp-go-link-target "pkg.go.dev"
                lsp-go-use-placeholders t)

  ;; Experimental settings
  ;; https://github.com/golang/tools/blob/master/gopls/doc/settings.md
  ;; anything marked as, "This setting is experimental and may be
  ;; deleted."
  (lsp-register-custom-settings '(("gopls.staticcheck" t t)
                                  ("go.inlayHints.assignVariableTypes" t t)
                                  ("go.inlayHints.compositeLiteralFields" t t)
                                  ("go.inlayHints.compositeLiteralTypes" t t )
                                  ("go.inlayHints.constantValues" t t)
                                  ("go.inlayHints.functionTypeParameters" t t)
                                  ("go.inlayHints.parameterNames" t t)
                                  ("go.inlayHints.rangeVariableTypes" t t))))

highlight-indent-guides

(use-package highlight-indent-guides
  :ensure t
  :config
  (setq highlight-indent-guides-method 'character
        highlight-indent-guides-responsive 'stack))

highlight-parentheses

(use-package highlight-parentheses
  :ensure t
  :hook ((emacs-lisp-mode . highlight-parentheses-mode)
         (lisp-mode-hook . highlight-parentheses-mode)))

htmlize

(use-package htmlize
  :ensure t)

ibuffer

(require 'face-remap)

(defalias 'list-buffers 'ibuffer)

(use-package ibuffer
  :custom
  (ibuffer-formats '((mark " "
                           (modified)
                           " "
                           (name 40 40 :right :elide)
                           " "
                           (filename-and-process))
                     (mark " "
                           (filename-and-process 70 70 :left :elide)
                           " "
                           name))))

IELM

(use-package ielm
  :hook ((ielm-mode . eldoc-mode)
         (ielm-mode . paredit-mode)))

imenu

Re-scan the buffer for new menu items automatically.

(use-package imenu
  :config
  (setq-default imenu-auto-rescan t))

I'm Feeling Lucky

This is my Google search module.

(use-package im-feeling-lucky
  :ensure nil
  :load-path "site-lisp/im-feeling-lucky.el"
  :bind (("H-l" . ifl-region-or-query)))

js

(use-package js
  :mode ("\\.json\\'" . js-mode)
  :hook ((js-mode . flycheck-mode)))

LaTeX

(use-package tex-mode
  :ensure t
  :bind (:map latex-mode-map ("C-j" . join-line)))

ligature-pragmatapro

(use-package ligature-pragmatapro
  :ensure t
  :config
  (ligature-pragmatapro-setup)
  (global-ligature-mode t))

lockfiles

Avoid creating temporary symbolic links and disturbing working directory state at the expense of avoiding editing collisions that I do not ever anticipate.

(setq create-lockfiles nil)

lsp-mode

(use-package lsp-mode
  :ensure t
  :config
  (setq-default
   lsp-eldoc-render-all nil
   lsp-enable-snippet nil
   lsp-signature-doc-lines 20           ; limit lines shown in docs
   lsp-enable-links nil                 ; disable clickable links in files
   )
  (set-face-attribute 'lsp-face-highlight-textual nil :underline t))

lua-mode

(use-package lua-mode
  :ensure t)

man

Setting a width avoids a possibly (likely) poorly chosen automatic width.

(setq-default Man-width 80)

markdown-mode

(use-package markdown-mode
  :ensure t
  :config (setq markdown-open-command "open")
  :hook ((markdown-mode . prettify-symbols-mode)))

Minibuffer

Scale up the minibuffer text size and limit how tall it can get.

(defun k20e/minibuffer-setup-hook ()
  "Bump up minibuffer text size and height."
  (text-scale-set 3)
  (setq max-mini-window-height 20))

(add-hook 'minibuffer-setup-hook 'k20e/minibuffer-setup-hook)

Set enable-recursive-minibufers to t to allow minibuffers within minibuffers. A good use-case of this feature is described in Executing Shell Commands in Emacs.

(setq enable-recursive-minibuffers t)

Eval expression minibuffer

Enable eldoc in the modeline.

(require 'eldoc)

(defun k20e/eval-expression-minibuffer-setup-hook ()
  (eldoc-mode 1))

(add-hook 'eval-expression-minibuffer-setup-hook
          'k20e/eval-expression-minibuffer-setup-hook)

multiple-cursors

(use-package multiple-cursors
  :ensure t
  :bind (("M-L" . mc/edit-lines)
         ("C-M-." . k20e/mark-next)
         ("C-M-," . k20e/mark-previous)
         ("C-M-<return>" . mc/mark-all-like-this))
  :config
  (defun k20e/mark-next (extended)
    "Wrap multiple-cursors mark-more/next.
Call `mc/mark-next-like-this' without a prefix argument.
Argument EXTENDED Prefix argument to call function
`mc/mark-more-like-this-extended'."
    (interactive "P")
    (if extended
        (call-interactively 'mc/mark-more-like-this-extended)
      (call-interactively 'mc/mark-next-like-this)))

  (defun k20e/mark-previous (extended)
    "Wrap multiple-cursors mark-more/previous.
Call `mc/mark-previous-like-this' without a prefix argument.
Argument EXTENDED Prefix argument to call function
`mc/mark-more-like-this-extended'."
    (interactive "P")
    (if extended
        (call-interactively 'mc/mark-more-like-this-extended)
      (call-interactively 'mc/mark-previous-like-this))))

Keep preferences sync'd across machines.

(setq mc/list-file (expand-file-name ".mc-lists.el" k20e/google-drive-directory))

olivetti

olivetti is a minimal minor mode to provide a more distraction free writing environment.

(use-package olivetti
  :ensure t
  :config (setq olivetti-style 'fancy))

Open Source Licenses

(defun k20e/insert-mit-license ()
  "Insert MIT license file contents.
Populate the current year and user name."
  (interactive)
  (with-current-buffer (get-buffer-create "LICENSE.txt")
    (insert (format "The MIT License (MIT)

Copyright (c) %s %s

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the \"Software\"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
" (format-time-string "%Y") (user-full-name)))))

org-mode

(use-package org
  :custom-face
  (variable-pitch ((t (:family "ETBembo" :height 1.3))))
  (fixed-pitch ((t (:family "PragmataPro Mono Liga" :height 0.9))))
  (org-block ((t (:inherit fixed-pitch))))
  (org-block-begin-line ((t (:inherit fixed-pitch))))
  (org-block-end-line ((t (:inherit fixed-pitch))))
  (org-code ((t (:inherit (shadow fixed-pitch)))))
  (org-date ((t (:inherit (shadow fixed-pitch)))))
  (org-document-info-keyword ((t (:inherit (shadow fixed-pitch)))))
  (org-done ((t (:inherit (shadow fixed-pitch)))))
  (org-drawer ((t (:inherit (shadow fixed-pitch)))))
  (org-ellipsis ((t (:inherit (shadow fixed-pitch))))) ; "CANCELED" for some reason is this
  (org-indent ((t (:inherit (org-hide fixed-pitch)))))
  (org-meta-line ((t (:inherit (font-lock-comment-face fixed-pitch)))))
  (org-property-value ((t (:inherit fixed-pitch))))
  (org-quote ((t (:inherit (shadow)))))
  (org-special-keyword ((t (:inherit (font-lock-comment-face fixed-pitch)))))
  (org-table ((t (:inherit fixed-pitch))))
  (org-tag ((t (:inherit (shadow fixed-pitch)))))
  (org-todo ((t (:inherit (shadow fixed-pitch)))))
  (org-verbatim ((t (:inherit (shadow fixed-pitch)))))
  :hook
  ((org-mode . auto-fill-mode)
   (org-mode . prettify-symbols-mode)
   (org-mode . variable-pitch-mode)
   (org-mode . (lambda () (visual-line-mode 0))  ; TODO how to do this w/ :hook?
             ))
  :config
  (setq org-default-notes-file (expand-file-name "notes.org" org-directory)
        org-directory (expand-file-name "org" k20e/google-drive-directory)
        org-ellipsis "…"
        org-fontify-quote-and-verse-blocks t
        org-fontify-whole-heading-line t
        org-hide-emphasis-markers t
        org-log-redeadline 'time
        org-log-reschedule 'time
        org-outline-path-complete-in-steps nil
        org-pretty-entities t
        org-special-ctrl-a/e t
        org-use-speed-commands t
        truncate-lines nil)
  (setq-default org-adapt-indentation t
                org-goto-interface 'outline-path-completion
                org-startup-folded t)
  (setq-local global-hl-line-mode nil)
  :functions
  company-indent-or-complete-common
  company-select-next
  company-select-previous
  lsp-format-buffer
  lsp-organize-imports
  lsp-register-custom-settings)

UPGRAYEDD

cd ~/.emacs.d/site-lisp/org-mode
g fetch origin --tags
g co -b release_9.6.12 release_9.6.12
make clean
make
cd ~/.emacs.d
g add site-lisp/org-mode
g commit -m "UPGRAYEDD org-mode"

contrib/ was split out into a separate repo so that needs to be updated separately.

Inline Images

Try to get the width of images displayed inline from a #+ATTR.* keyword, e.g., #+ATTR_HTML: :width 800px, fall back to original image width if no attribute keyword is found:

(setq org-image-actual-width nil)

Key Bindings

(global-set-key (kbd "<f12>") 'org-agenda-list)
(global-set-key (kbd "C-c a") 'org-agenda)
(global-set-key (kbd "C-c l") 'org-store-link)
(global-set-key (kbd "C-x c") 'org-switchb)

(define-key org-mode-map (kbd "<return>") 'org-return-indent)
(define-key org-mode-map (kbd "M-<return>") 'org-meta-return)
(define-key org-mode-map (kbd "C-j") 'join-line)
(define-key org-mode-map (kbd "C-m") 'org-return-indent)
(define-key org-mode-map (kbd "H-<tab>") 'pcomplete)
(define-key org-mode-map (kbd "M-h") 'backward-kill-word)

Export

Most non-interactive export settings are defined in a file loaded during initialization. Those settings are defined during initialization time to support a fast batch process for exporting this document to HTML in a Git post-commit hook.

(require 'k20e-org-html-export)

Interactive customization can be done here.

(require 'ox-publish)

(setq
 ;; Enable "expert" export interface.
 org-export-dispatch-use-expert-ui t

 ;; Continue export when links are broken, but mark them
 org-export-with-broken-links `mark)

Options

A message in *Messages* like:

user-error: Unable to resolve link: nil

indicates that a link somewhere is malformed. Adding the option:

#+OPTIONS: broken-links:mark

and exporting will insert BROKEN LINK into the HTML document. Searching for that token makes finding the offending broken link much easier. Keeping this option set all the time would let broken links slip through the export process undetected.

Backends

(require 'ox-md)

(add-to-list 'org-export-backends 'md)
(use-package ox-tufte :ensure t)

Publish

(setq org-publish-project-alist
      `(("k20e.com-org-files"
         :base-directory ,(expand-file-name "source" (expand-file-name "k20e.com" k20e/google-drive-directory))
         :base-extension "org"
         :recursive t
         :exclude "ga.org\\|level-0.org\\|todo.org\\|.DS_Store"
         :publishing-directory ,(expand-file-name "published" (expand-file-name "k20e.com" k20e/google-drive-directory))
         :publishing-function org-html-publish-to-html
         :with-planning t)
        ("k20e.com-static-files"
         :base-directory ,(expand-file-name "source" (expand-file-name "k20e.com" k20e/google-drive-directory))
         :base-extension "jpg\\|png\\|ico"
         :recursive t
         :publishing-directory ,(expand-file-name "published" (expand-file-name "k20e.com" k20e/google-drive-directory))
         :publishing-function org-publish-attachment)
        ("k20e.com"
         :components ("k20e.com-org-files" "k20e.com-static-files"))
        ("work-org-files"
         :base-directory ,(expand-file-name "work" org-directory)
         :base-extension "org"
         :publishing-directory ,(expand-file-name "published" (expand-file-name "work" org-directory))
         :publishing-function org-html-publish-to-html
         :with-planning t)
        ("work-static-files"
         :base-directory ,(expand-file-name "work" org-directory)
         :base-extension "pdf\\|csv\\|sql\\|png"
         :publishing-directory ,(expand-file-name "published" (expand-file-name "work" org-directory))
         :publishing-function org-publish-attachment)
        ("work"
         :components ("work-org-files" "work-static-files"))
        ("house-org-files"
         :base-directory ,(expand-file-name "house" org-directory)
         :base-extension "org"
         :recursive t
         :publishing-directory ,(expand-file-name "published" (expand-file-name "house" org-directory))
         :publishing-function org-html-publish-to-html
         :with-planning t)
        ("house-static-files"
         :base-directory ,(expand-file-name "house" org-directory)
         :base-extension "pdf\\|csv\\|png\\|xls\\|doc"
         :recursive t
         :publishing-directory ,(expand-file-name "published" (expand-file-name "house" org-directory))
         :publishing-function org-publish-attachment)
        ("house"
         :components ("house-org-files" "house-static-files"))))

Babel

Define which languages org-babel should support.

(defvar k20e/org-babel-load-languages
  '((ditaa . t)
    (emacs-lisp . t)
    (js . t)
    (org . t)
    (python . t)
    (scheme . t)
    (shell . t)
    (sql . t))
  "Languages to evaluate in `org-mode'.")

(org-babel-do-load-languages 'org-babel-load-languages
                             k20e/org-babel-load-languages)

Disable interactive prompt for executing code blocks. This is dangerous but I never execute any org files that I didn't author.

(setq org-confirm-babel-evaluate nil)

TODO Items

Automatically insert a timestamp when a task is marked DONE.

(setq org-log-done t)

Custom keywords and faces.

(setq org-todo-keywords '((sequence
                           "TODO(t)"
                           "STARTED(s!)"
                           "|"
                           "DONE(d!)"
                           "CANCELED(c@)"))
      org-todo-keyword-faces '(("TODO" . org-todo)
                               ("STARTED" . org-code)
                               ("CANCELED" . org-ellipsis)
                               ("DONE" . org-done)))

Agenda

(require 'face-remap)
(require 'org)
(require 'org-agenda)
(require 'winner)

(defun k20e/org-agenda-mode-hook ()
  (define-key org-agenda-mode-map (kbd "q")
    (lambda (x)
      (interactive "p")
      (winner-undo)
      (kill-buffer "*Org Agenda*")))
  (delete-other-windows)
  (text-scale-set 2))

(add-hook 'org-agenda-mode-hook 'k20e/org-agenda-mode-hook)

Files

(setq org-agenda-files (list (expand-file-name "work" org-directory)))

Deadlines

Non-nil means skip scheduling line if same entry shows because of deadline.

In the agenda of today, an entry can show up multiple times because it is both scheduled and has a nearby deadline, and maybe a plain time stamp as well.

When set to t, then only the deadline is shown and the fact that the entry is scheduled today or was scheduled previously is not shown.

(setq org-agenda-skip-scheduled-if-deadline-is-shown nil)

List

Default to showing only today in the agenda list.

(setq org-agenda-span 'day)

Habit

(require 'org-habit)

(setq org-habit-completed-glyph ?✓
      org-habit-today-glyph ?|)

Logging & Drawers

Insert state change notes and time stamps into a drawer rather than simply "loose" after a headline.

(setq org-log-into-drawer t)

Clock

(defvar org-clock-idle-time 5)

Superstar

Successor to org-bullets, org-superstar-mode.

(use-package org-superstar
  :ensure t
  :custom
  (setq org-superstar-special-todo-items t
        org-superstar-todo-bullet-alist '(("TODO" . 9744) ; 
                                          ("STARTED" . 9744) ; 
                                          ("DONE" . 9745) ; 
                                          ("CANCELED" . 10008))) ; 
  :hook ((org-mode . org-superstar-mode)))

TODO [0/1] Other Configuration

paredit-mode

(use-package paredit
  :ensure t
  :bind (:map paredit-mode-map
              ([?\)] . paredit-close-parenthesis)
              ([(meta ?\))] . paredit-close-parenthesis-and-newline)
              ("C-h" . paredit-backward-delete)
              ("C-j" . join-line))
  :hook ((emacs-lisp-mode . paredit-mode)
         (geiser-mode . paredit-mode)
         (geiser-repl-mode . paredit-mode)
         (lisp-mode . paredit-mode)
         (scheme-mode . paredit-mode)))

prettify-symbols-mode

python

(use-package blacken :ensure t)

(use-package python
  :ensure t
  :after (flycheck lsp-mode)
  :config
  (setq-default
   lsp-pylsp-plugins-black-enabled t)
  :bind (:map python-mode-map
              ("M-a" . python-nav-beginning-of-statement)
              ("M-e" . python-nav-end-of-statement)
              ("M-n" . python-nav-forward-statement)
              ("M-p" . python-nav-backward-statement)
              ("M-q" . blacken-buffer)
              ("C-." . completion-at-point)
              ("C-M-f" . python-nav-forward-sexp)
              ("C-M-b" . python-nav-backward-sexp)
              ("C-M-n" . python-nav-forward-block)
              ("C-M-p" . python-nav-backward-block))
  :hook ((before-save . (lambda ()
                          (when (eq major-mode 'python-mode)
                            (lsp-organize-imports)
                            (lsp-format-buffer))))
         (python-mode . blacken-mode)
         (python-mode . electric-pair-mode)
         (python-mode . flycheck-mode)
         (python-mode . lsp-deferred)
         (python-mode . prettify-symbols-mode)
         (python-mode . superword-mode)))

re-builder

(use-package re-builder
  :config
  (setq reb-re-syntax 'string))

recentf

(require 'recentf)

(use-package emacs
  :config
  (setq recentf-save-file (expand-file-name ".recentf" k20e/google-drive-directory)
        recentf-max-saved-items 250)
  (recentf-mode 1)
  :bind (("C-x C-r" . recentf-open)))

rust

(use-package flycheck :ensure t)
(use-package flycheck-rust :ensure t)
(use-package rust-mode
  :ensure t
  :config
  (defun k20e/rust-mode-hook ()
    (setq flycheck-checker 'rust
          rust-format-on-save t)
    (flycheck-mode 1)
    (flycheck-rust-setup))
  :hook ((rust-mode . k20e/rust-mode-hook)))

save-place-mode

(use-package emacs
  :init
  (save-place-mode t))

savehist

(use-package savehist
  :config
  (setq savehist-file (expand-file-name ".savehist" k20e/google-drive-directory)))

(savehist-mode)

*scratch*

Begin with an empty *scratch* file.

(setq initial-scratch-message nil)

Set it to Emacs Lisp mode.

(with-current-buffer (get-buffer-create "*scratch*")
  (emacs-lisp-mode))

Quickly create new scratch buffers

With a preset list of major modes that I find often need scratch pads for.

(defconst k20e/scratch-buffer-modes
  '(fundamental-mode
    emacs-lisp-mode
    python-mode
    javascript-mode
    org-mode
    sql-mode
    text-mode
    yaml-mode)
  "Common major modes to create scratch buffers for.")

(defun k20e/scratch-buffer ()
  "Generate a new scratch buffer.
Choose from `k20e/scratch-buffer-modes' list of major modes to
enable in the newly created scratch buffer and switch to it."
  (interactive)
  (let ((mode (read (completing-read "New *scratch* buffer with mode: "
                                         (mapcar (lambda (el) (format "%s" el))
                                                 k20e/scratch-buffer-modes)))))
    (switch-to-buffer (generate-new-buffer (format "*scratch-%s*" mode)))
    (funcall mode)))

Bind it globally.

(global-set-key (kbd "<f10>") 'k20e/scratch-buffer)

sh-mode

(use-package sh-script
  :after (flycheck)
  :functions flycheck-select-checker
  :config
  (defun k20e/sh-script-hook ()
    (flycheck-select-checker 'sh-shellcheck))
  :hook ((sh-mode . k20e/sh-script-hook)
         (sh-mode . flycheck-mode)
         (sh-mode . prettify-symbols-mode)))

server

Start the Emacs server if it not already running.

(require 'server)

(unless (server-running-p)
  (server-start))

smex

(use-package smex
  :ensure t
  :config
  (smex-initialize)
  (setq-default smex-save-file (expand-file-name ".smex-items" k20e/google-drive-directory)))

sql-mode

(use-package sql
  :config
  (setq sql-product 'mysql
        tab-width 4))

Theme

Always highlight the current line:

(use-package hl-line)
(use-package emacs
  :config
  (global-hl-line-mode t))

Simple stuff:

(use-package simple)
(use-package emacs
  :config
  (line-number-mode)
  (column-number-mode)
  (size-indication-mode))

Highlight matching parentheses:

(use-package paren)
(use-package emacs
  :config
  (show-paren-mode))

Blink the cursor:

(use-package frame)
(use-package emacs
  :config
  (setq-default blink-cursor-mode t))

Truncate lines and enable fringes to indicate truncated lines:

(use-package fringe)
(use-package emacs
  :config
  (setq-default
   truncate-lines t
   truncate-partial-width-windows nil)
  (fringe-mode))


No bell:

(use-package emacs
  :config
  (setq-default ring-bell-function 'ignore))

Make sure syntax highlighting is enabled:

(use-package font-core)
(use-package emacs
  :config
  (global-font-lock-mode))

Set theme automatically on MacOS thanks to https://github.com/d12frosted/homebrew-emacs-plus#system-appearance-change. Just set it to dark on Linux for now.

(use-package color-theme-sanityinc-tomorrow
  :ensure t
  :config
  (cond ((eq system-type 'darwin)
         (defun k20e/load-theme (mode)
           "Load light or dark theme depending on which mode the system is currently in."
           (pcase mode
             ('light (load-theme 'sanityinc-tomorrow-day t))
             ('dark (load-theme 'sanityinc-tomorrow-bright t))))
         (add-hook 'ns-system-appearance-change-functions 'k20e/load-theme))
        ((string-equal system-type "gnu/linux")
         (load-theme 'sanityinc-tomorrow-bright t))))

terraform-mode

(use-package terraform-mode
  :ensure t
  :hook ((terraform-mode . terraform-format-on-save-mode)))

text-mode

(use-package simple
  :hook ((text-mode . auto-fill-mode)))

toml-mode

(use-package toml-mode
  :ensure t
  :mode ("Pipfile\\'" . toml-mode))

tramp

ControlPath

Fix ControlPath too long errors due to OS X pitching a long temporary directory to ssh.

Unfortunately, setting this is blowing up the server-start which can no longer find the socket stored in the original TMPDIR value.

;; (setenv "TMPDIR" "/tmp")

Eureka! It appears that the ControlMaster option for ssh should be set to yes instead of auto to avoid the ControlPath too long error. Here is the interesting section of man 5 ssh_config:

ControlMaster
             Enables the sharing of multiple sessions over a single network connection.  When set to
             ``yes'', ssh(1) will listen for connections on a control socket specified using the
             ControlPath argument.  Additional sessions can connect to this socket using the same
             ControlPath with ControlMaster set to ``no'' (the default).  These sessions will try to reuse
             the master instance's network connection rather than initiating new ones, but will fall back
             to connecting normally if the control socket does not exist, or is not listening.

             Setting this to ``ask'' will cause ssh to listen for control connections, but require confir-
             mation using the SSH_ASKPASS program before they are accepted (see ssh-add(1) for details).
             If the ControlPath cannot be opened, ssh will continue without connecting to a master
             instance.

             X11 and ssh-agent(1) forwarding is supported over these multiplexed connections, however the
             display and agent forwarded will be the one belonging to the master connection i.e. it is not
             possible to forward multiple displays or agents.

             Two additional options allow for opportunistic multiplexing: try to use a master connection
             but fall back to creating a new one if one does not already exist.  These options are:
             ``auto'' and ``autoask''.  The latter requires confirmation like the ``ask'' option.

The tramp-ssh-controlmaster-options variable is responsible for the ControlMaster value as well as a few other options which have not been changed from their default values.

(use-package tramp
  :defines tramp-ssh-controlmaster-options
  :config
  (setq tramp-ssh-controlmaster-options "-o ControlPath=%t.%%r@%%h:%%p -o ControlMaster=yes -o ControlPersist=no"))

Inline Copying

Do not inline copy files. This is to avoid File exists, but cannot be read errors.

(setq-default tramp-copy-size-limit -1)

File Backup

Do not backup files edited by tramp to avoid possibly sharing copies of privileged files with non-privileged users.

(add-to-list 'backup-directory-alist (list tramp-file-name-regexp))

Debugging

Level 3 by default.

;; (setq tramp-verbose 6)

Will create a detailed log buffer.

uniquify

Name multiple identical buffer names in a sensible manner.

(use-package uniquify
  :config
  (setq uniquify-buffer-name-style 'forward))

vertico

Since vertico is meant to be plug & play, I'm bundling the extension configuration under this section, as well.

(use-package vertico
  :ensure t
  :init
  (vertico-mode)
  (setq vertico-cycle t
        vertico-preselect 'first))

vertico-directory

vertico-directory is an extension which provides Ido-like directory navigation. It is vendored in the vertico repo. so do not :ensure t here.

(use-package vertico-directory
  :after vertico
  :demand
  :bind (:map vertico-map
              ("RET" . vertico-directory-enter)
              ("DEL" . vertico-directory-delete-char)
              ("M-DEL" . vertico-directory-delete-word))
  :hook (rfn-eshadow-update-overlay . vertico-directory-tidy))

orderless

orderless is an alternative completion style extension.

(use-package orderless
  :ensure t
  :init
  (setq completion-styles '(orderless basic)))

consult

consult is a completing-read extension.

(use-package consult
  :ensure t
  :demand
  :bind (("C-s" . consult-line)
         ("C-x b". consult-buffer)
         ("C-x C-a" . consult-ripgrep)
         ("C-x C-g" . consult-git-grep)
         :map org-mode-map
         ("C-c C-j" . consult-org-heading)))

corfu

corfu is an in-buffer completion popup extension.

(use-package corfu
  :ensure t
  :custom (setq corfu-cycle t)
  :init (global-corfu-mode))

(use-package emacs
  :init (setq tab-always-indent 'complete))

TODO [0/2] Other Configuration

marginalia

marginalia is an extension that adds information as annotations in the minibuffer.

(use-package marginalia
  :ensure t
  :after vertico
  :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle))
  :init (marginalia-mode))

windmove

(use-package windmove
  :config
  (windmove-default-keybindings 'super)
  (setq windmove-wrap-around t)
  :bind (("M-SPC" . windmove-right)
         ("M-S-SPC" . windmove-left)))

web-mode

(use-package elec-pair
  :functions electric-pair-default-inhibit)

(use-package web-mode
  :ensure t
  :functions flycheck-add-mode
  :after (elec-pair flycheck)
  :mode
  ("\\.html?\\'" . web-mode)
  ("\\.jsx?\\'" . web-mode)
  ("\\.tsx?\\'" . web-mode)
  :config
  (defun k20e/web-mode-hook ()
    (set-default 'web-mode-engines-alist '(("django" . "\\.html?\\'")))
    (setq web-mode-markup-indent-offset 2
          web-mode-enable-auto-quoting nil)

    (flycheck-add-mode 'typescript-tslint 'web-mode)

    ;; Disable pairing { to avoid {{ }}}} from web-mode block templates
    (setq-local electric-pair-inhibit-predicate
                (lambda (c)
                  (if (char-equal c ?{) t (electric-pair-default-inhibit c)))))
  :hook ((web-mode . k20e/web-mode-hook)))

winner-mode

Remember window configurations.

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

whitespace

Take care of some whitespace issues.

  • Kill trailing whitespace on save
  • Insert a new line at the end of file on save
  • Prefer space over tab
(use-package whitespace
  :ensure t
  :config
  (setq-default require-final-newline t
                mode-require-final-newline t
                indent-tabs-mode nil)
  (defun k20e/before-save-hook ()
    (whitespace-cleanup)
    (delete-trailing-whitespace 0 nil))
  :hook ((before-save . k20e/before-save-hook)))

yaml-mode

(use-package flycheck-yamllint
  :ensure t
  :after (flycheck))

(use-package highlight-indent-guides
  :ensure t)

(use-package yaml-mode
  :ensure t
  :after (flycheck-yamllint flyspell highlight-indent-guides)
  :bind (:map yaml-mode-map ("C-m" . newline-and-indent))
  :config
  (defun k20e/yaml-mode-hook ()
    (flycheck-select-checker 'yaml-yamllint)
    (flycheck-yamllint-setup)
    (flyspell-mode-off))
  :hook ((yaml-mode . k20e/yaml-mode-hook)
         (yaml-mode . flycheck-mode)
         (yaml-mode . highlight-indent-guides-mode)
         (yaml-mode . prettify-symbols-mode)))

zig-mode

(use-package zig-mode
  :ensure t
  :bind
  (:map zig-mode-map ("C-." . 'company-indent-or-complete-common))
  :functions
  lsp-register-client
  make-lsp-client
  :hook
  ((before-save . (lambda () (when (eq major-mode 'zig-mode)
                          (lsp-format-buffer))))
   (zig-mode . company-mode)
   (zig-mode . lsp-deferred))
  :custom (zig-format-on-save nil)
  :config
  (lsp-register-client
   (make-lsp-client
    :major-modes '(zig-mode)
    :server-id 'zls)))

zig config then modified a bit to arrive at:

{
  "enable_semantic_tokens": true,
  "enable_snippets": true,
  "include_at_in_builtins": false,
  "include_at_in_builtins": true,
  "max_detail_length": 1048576,
  "operator_completions": true,
  "warn_style": true,
  "zig_exe_path": "/bin/zig",
  "zig_lib_path": "/usr/lib/zig"
}

Kris Molendyke

2023-12-28 Thu 08:55

Emacs 29.1 (Org mode 9.6.12)