Skip to content

renzmann/.emacs.d

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

My Literate .emacs.d

Want to use it? Go ahead! You can give it a try without affecting any existing configuration like this:

git clone https://github.com/renzmann/.emacs.d ~/renzmann.emacs.d
emacs --init-directory=~/renzmann.emacs.d

All external dependencies, except for Fonts, are explicitly included under the elpa/ directory, meaning it’s as simple as “clone-n-go”. Getting a font you like is left to you.

Notable Features

This configuration is largely based on tools designed to integrate well with Emacs 29+’s built-in APIs. See sections for “Tools” and “Text completion” for up-to-date commentary on what I’m choosing lately that fits this description.

Most of my time is spent in Org, SQL, Python, Bash, YAML, TOML, and Markdown, so the majority of configuration lies around these sections.

I do make changes to things that I feel “should have been included.” Some examples of this are:

  1. Additional major modes for common file types like Markdown and CSV
  2. Error message support for pyright in a *Compilation* buffer
  3. Reasonable indentation behavior for SQL files
  4. Updating buffers automatically if their contents change on disk
  5. Handling ANSI color escape codes in shell output, compilation, and VC buffers

Tangling

My configuration is a single literate programming document, which is tangled into the standard init.el and supporting files. This is so I can keep track of all the crazy things I try, and explain them inline with the final code I decide to include. Some platforms like GitHub can render this document in a limited way, but to see all the final configuration values I use you will likely have to view this document in Emacs itself.

Why use a literate document for my configuration? Basically, as I added more comments and reminders about what some line of code was doing, where I got it from, and why it might be commented out, the prose grew longer than the actual code, and so a change of medium felt prudent. In my case, that’s the venerable Org mode, which comes with Emacs and serves as a way to seamlessly weave commentary and code together.

Inspirations

I steal quite a lot from other, more qualified Emacs community contributors, such as:

Getting Emacs

Windows

I go to the pretest FTP to get the latest version of Emacs. Usually not quite up-to-date with the master branch, but still one version number ahead of the most recent official release on the main FTP.

Mac

On macOS, I’ve had the best luck with jimeh’s nightly builds. These Emacs.app bundles have no external dependencies, signed with a developer certificate, and notarized by Apple, so it just works. Even without administrator permissions, you can drag the bundle to the “Applications” folder under your user home instead, and Emacs still works beautifully.

In particular, this feature has saved me a lot of headaches that I ran into compiling Emacs on my own:

Emacs.app is signed with a developer certificate and notarized by Apple.

Very nice!

Linux

Often the version of Emacs that comes through my system package manager, such as sudo apt-get install emacs, is out of date. So, typically, I’ll get a release tarball from the FTP and compile it myself. On Debian, that requires an apt-get install of the following dev libraries:

sudo apt-get install \
    autoconf \
    automake \
    build-essential \
    curl \
    libgccjit-12-dev \
    libgif-dev \
    libgnutls28-dev \
    libgtk-4-dev \
    libgtk2.0-dev \
    libjpeg-dev \
    libncurses-dev \
    libpng-dev \
    libtiff-dev \
    libtree-sitter-dev \
    libx11-dev \
    libxpm-dev \
    libxpm-dev \
    make \
    texinfo

I typically run into trouble (different every time) when installing to system-wide locations like /usr or /usr/local, so I’ll just opt for a user install under ~/.local.

curl -O https://ftp.gnu.org/gnu/emacs/emacs-29.3.tar.xz
tar xf emacs-29.3.tar.xz
cd emacs-29.3
./configure \
  --prefix=$HOME/.local \
  --with-native-compilation \
  --with-tree-sitter \
  --with-gnutls \
  --with-jpeg \
  --with-png \
  --with-rsvg \
  --with-tiff \
  --with-wide-int \
  --with-xft \
  --with-xml2 \
  --with-xpm \
  --without-dbus \
  --without-pop
make -j
make install

Header

To comply with the Emacs conventions for libraries, the tangled init.el must have the following header and footer:

;;; init.el --- Robb's Emacs configuration -*- lexical-binding: t -*-

;; Copyright (C) 2022 Robert Enzmann

;; Author: Robb Enzmann <[email protected]>
;; Keywords: internal
;; URL: https://robbmann.io/

;;; Commentary:
;; A mostly minimal, reproducible Emacs configuration.  This file is
;; automatically tangled from README.org, with header/footer comments on each
;; code block that allow for de-tangling the source back to README.org when
;; working on this file directly.

;;; Code:

Custom

I prefer having custom modify its own file. This next snippet ensures any package-install or custom edits go to custom.el.

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
  (load custom-file 'noerror))

Proxy settings

When behind a corporate proxy, we might have to authenticate before we can pull packages off ELPA. Emacs only uses the HOST and PORT portions of the http_proxy and https_proxy environment variables, so we need to set LOGIN (user id) and PASSWORD ourselves.

I store the login, port, and host variables in a proxy.el file (obviously outside version control) when I’m on a machine that’s behind an http proxy. We grab the password interactively when such a file exists.

(defun renz/enable-proxy ()
  (interactive)
  "Turn on HTTP proxy."
  (let ((proxy-file (expand-file-name "proxy.el" user-emacs-directory)))
    (when (file-exists-p proxy-file)
      (load-file proxy-file)
      (setq url-proxy-services
            `(("no_proxy" . "^\\(localhost\\|10.*\\)")
              ("http" . ,(concat renz/proxy-host ":" renz/proxy-port))
              ("https" . ,(concat renz/proxy-host ":" renz/proxy-port))))
      (setq url-http-proxy-basic-auth-storage
            (list
             (list
              (concat renz/proxy-host ":" renz/proxy-port)
              (cons renz/proxy-login
                    (base64-encode-string
                     (concat renz/proxy-login ":" (password-read "Proxy password: "))))))))))

(defun renz/disable-proxy ()
  (interactive)
  "Turn off HTTP proxy."
  (setq url-proxy-services nil)
  (setq url-http-proxy-basic-auth-storage nil))

Packages

The initial cornerstone of every Emacs configuration is a decision on package management and configuration. I opt for use-package and package.el, since both are built-in to Emacs 29+, which helps maximize stability and portability.

To avoid loading packages twice, the manual recommends disabling package-enable-at-startup in init.el.

(require 'package)
(setq package-enable-at-startup nil)
(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/") t)

I do not use the :ensure t keyword in use-package declarations to install packages, because I cannot always ensure that I have a stable connection to GNU ELPA (in the case of package-install-selected-packages) or the public github.com (for package-vc-install-selected-packages). Instead, I rely on M-x package-install and M-x package-delete, and only permit use-package to handle the configuration and loading of packages. As mentioned in the introduction, each package’s source is explicitly included into version control of my configuration, so I don’t worry too much about pinning package versions in this file. When I want to update a package, I use M-x package-update, the package.el user interface, or delete the package’s source folder and use renz/package-sync (defined below). Should something go wrong, I roll back to a previous commit. So far, this method has been reliable for keeping my init.el (this README), custom.el, the package-selected-packages variable, and elpa/ directory all in sync with one another.

First thing’s first, though; I need a way within my lisp code to tell if we’re running Windows.

(defun renz/windowsp ()
  "Are we on Microsoft Windows?"
  (memq system-type '(windows-nt cygwin ms-dos)))

One “feature” of MSYS is that paths take on a unix-like format. So C:/Users/... becomes /c/Users/.... The MSYS installation of gpg, which is what Emacs would use to verify package signatures when running under MSYS, only undertands this latter expansion; and not the former style of Windows path (even though Emacs itself is perfectly happy with them).

(when-let* ((on-win (renz/windowsp))
            (has-uname (executable-find "uname"))
            (uname (shell-command-to-string "uname"))
            (is-msys (string-prefix-p "MSYS" uname))
            (package-dir-expandable (string-prefix-p "~" package-user-dir))
            (expand-package-dir (expand-file-name "gnupg" package-user-dir))
            (new-package-user-dir (replace-regexp-in-string "^\\([a-zA-Z]\\):/" "/\\1/" expand-package-dir)))
  (setq package-gnupghome-dir new-package-user-dir))

Without modifying the package-user-dir in this way, we get a nasty error like this.

With all that out of the way, though, we can sync up our packages.

(defun renz/package-sync ()
  "Remove unused sources and install any missing ones."
  (interactive)
  (package-autoremove)
  (package-install-selected-packages)
  (package-vc-install-selected-packages))

There are also a few hand-made packages I keep around in a special .emacs.d/site-lisp directory.

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

OS-specific Configuration

Microsoft Windows

While usable out of the box, Emacs will be far less productive without some additional setup on a Windows machine. Then, there are a few things I set up independent of Emacs. Namely, find, xargs, gcc, and rg. Even with all of this setup in place, it’s still tough to get Powershell to play nicely with some of the shell quoting Emacs has to do when running external programs like find and grep. For instance, if we were to set the shell-file-name like this:

(when (and (renz/windowsp) (executable-find "pwsh"))
  (setq shell-file-name "pwsh"))

Then running C-u C-x p f (project-find-file), which augments the find command to include files that would normally be ignored by .gitignore, we get this nasty message:

project--files-in-directory: File listing failed: -path: The term '-path' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

If we don’t have admin privilages, there are some lisp alternatives to find and grep, such as M-x find-lisp-find-dired

With a little work, we can make a completing-read find files using lisp:

(defun renz/find-file (chosen-dir regex)
  (interactive "DSearch dir: \nsRegexp: ")
  (let ((chosen-file (completing-read "File: " (find-lisp-find-files chosen-dir regex))))
    (find-file chosen-file)))

(global-set-key (kbd "C-c f f") #'renz/find)
(global-set-key (kbd "C-c f d") #'find-lisp-find-dired)

With a little polish this might actually even work well.

On the winkey

Windows, funnily enough, has some trouble registering the Windows key as a usable modifier for Emacs. In fact, s-l will never be an option, since it’s handled at the hardware level. For a time I considered enabling the use of the winkey like this:

(setq w32-pass-lwindow-to-system nil)
(setq w32-lwindow-modifier 'super) ; Left Windows key
(setq w32-pass-rwindow-to-system nil)
(setq w32-rwindow-modifier 'super) ; Right Windows key

Followed by enabling specific chords, such as “winkey+a”:

(w32-register-hot-key [s-a])

Since I’ve taken a more TTY-friendly approach for my config in general, where super can be a bit tough to integrate with both the windowing application and the terminal emulator, I’ve mostly given up on the GUI key in favor of other chords, especially the C-c ones.

macOS

Launching Emacs from the typical application launcher or command-space usually won’t capture any modifications to $PATH, typically handled in a file like ~/.profile or ~/.bashrc. So, the main configuration included here is from exec-path-from-shell.

(when (eq system-type 'darwin)
  (setq exec-path-from-shell-arguments '("-l"))
  (exec-path-from-shell-initialize))

Font

Fonts are a tricky business. See Emacs/Fonts in the manual (C-h i) for relevant information on how checking and setting default fonts works:

(cond ((x-list-fonts "Hack Nerd Font")
       (add-to-list 'default-frame-alist '(font . "Hack Nerd Font-12")))
      ;; ((x-list-fonts "Segoe UI Emoji")
      ;;  (add-to-list 'default-frame-alist '(font . "Segoe UI Emoji-12")))
      )

If the font is the wrong size, starting with Emacs 29.1 you can use C-x C-M-+ and C-x C-M-- to change the size globally.

Theme

Hats off to Prot for his wonderful themes.

(load-theme 'modus-vivendi t)

Window margins and fringe

This hunk adds some space around all sides of each window so that we get a clear space between the edge of the screen and the fringe.

(defun renz/modify-margins ()
  "Add some space around each window."
  (interactive)
  (modify-all-frames-parameters
   '((right-divider-width . 40)
     (internal-border-width . 40)))
  (dolist (face '(window-divider
                  window-divider-first-pixel
                  window-divider-last-pixel))
    (face-spec-reset-face face)
    (set-face-foreground face (face-attribute 'default :background)))
  (set-face-background 'fringe (face-attribute 'default :background)))

(renz/modify-margins)

Emacs’ Built-in Settings

My settings for base Emacs behavior. Assuming I ran with no plugins (ala emacs -Q), I would still set most of these by hand at one point or another. This section is designed for variables that modify Emacs and its editing behavior directly. Configuration for built-in tools, such as Dired, Tramp, and Tree-sitter are located under Tool configuration.

Stop stupid bell

This snippet has a special place in my heart, because it was the first two lines of elisp I wrote when first learning Emacs. It is the kernel around which my ~/.emacs and later ~/.emacs.d/init.el grew.

;; Stop stupid bell
(setq ring-bell-function 'ignore)

The bell is really, really annoying.

Start a server for emacsclient

(server-start)

Don’t hang when visiting files with extremely long lines

(global-so-long-mode t)

Unicode

Sometimes (especially on Windows), Emacs gets confused about what encoding to use. These settings try to prevent that confusion.

(prefer-coding-system       'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(setq default-buffer-file-coding-system 'utf-8)
(setq x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING))

Mode line

It’s easy for the mode line to get cluttered once things like Flymake and eglot kick in. When I was starting out, I used to have these two settings:

(setq display-battery-mode t
      display-time-day-and-date t)

(display-time)

After a while I noticed that I’m almost never running Emacs in a full screen where I can’t see the battery or date in the corner of my window manager, so they were just wasting mode line space. Nowadays I simply opt for column mode and a dimmed mode line in non-selected windows.

(setq column-number-mode t
      mode-line-in-non-selected-windows t)

Remember minibuffer history

Found this on a System Crafters video.

(savehist-mode 1)

Render ASCII color escape codes

For files containing color escape codes, this provides a way to render the colors in-buffer. Provided by a helpful stackoverflow answer.

(defun renz/display-ansi-colors ()
  "Render colors in a buffer that contains ASCII color escape codes."
  (interactive)
  (require 'ansi-color)
  (let ((inhibit-read-only t))
    (ansi-color-apply-on-region (point-min) (point-max))))

Color output in eshell and *compilation*

In *compilation* mode, we just use the “display colors” function from above. Enable colors in the *compilation* buffer.

(add-hook 'compilation-filter-hook #'renz/display-ansi-colors)

For eshell, this is copy-pasted from a stack overflow question.

(add-hook 'eshell-preoutput-filter-functions  #'ansi-color-apply)

xterm-color

Soon, I’d like to swap out my hacks above for this more robust package: https://github.com/atomontage/xterm-color/tree/master

Recent files menu

This enables “File -> Open Recent” from the menu bar and using completing-read over the recentf-list.

(recentf-mode t)

(defun renz/find-recent-file ()
  "Find a file that was recently visted using `completing-read'."
  (interactive)
  (find-file (completing-read "Find recent file: " recentf-list nil t)))

Fill-column

Regardless of whether we’re doing visual fill or hard fill, I like the default at around 80 characters, and I’ll manually change it per buffer if I want something different

(setq-default fill-column 80)

Scroll bar

I toggle this one on/off sometimes depending on how I feel and which OS I’m currently on.

(scroll-bar-mode -1)

By default, though, I prefer it to be off when I start Emacs.

Automatically visit symlink sources

When navigating to a file that is a symlink, this automatically redirects us to the source file it’s pointing to.

(setq find-file-visit-truename t)
(setq vc-follow-symlinks t)

Indent with spaces by default

For the most part I edit Python, SQL, Markdown, Org, and shell scripts. All of these favor spaces over tabs, so I prefer this as the default.

(setq-default indent-tabs-mode nil)

Generally, though, indentation behavior is set by major-mode functions, which may or may not use Emacs’ built-in indentation functions. For instance, when trying to find the functions behind indentation in shell mode, I came across smie.el, whose introductory comments include this gem:

OTOH we had to kill many chickens, read many coffee grounds, and practice untold numbers of black magic spells, to come up with the indentation code. Since then, some of that code has been beaten into submission, but the `smie-indent-keyword’ function is still pretty obscure.

Even the GNU Emacs manual speaks of it in the same way:

Writing a good indentation function can be difficult and to a large extent it is still a black art. Many major mode authors will start by writing a simple indentation function that works for simple cases, for example by comparing with the indentation of the previous text line. For most programming languages that are not really line-based, this tends to scale very poorly: improving such a function to let it handle more diverse situations tends to become more and more difficult, resulting in the end with a large, complex, unmaintainable indentation function which nobody dares to touch.

Enable horizontal scrolling with mouse

From a helpful stackoverflow answer.

(setq mouse-wheel-tilt-scroll t)

Window management

From a Mickey Petersen article, this causes switch-to-buffer to open the selected buffer in the current window rather than switching windows, assuming both are open in the current frame. This is more frequently the behavior I intend when I’m trying to get a window to display a specific buffer.

(setq switch-to-buffer-obey-display-actions t)

Automatically update buffers when contents change on disk

Without setting global-auto-revert-mode, we have to remember to issue a revert-buffer or revert-buffer-quick (C-x x g by default) in case a file changed. Over Tramp, we still have to manually revert files when they’ve changed on disk.

(global-auto-revert-mode)

Highlight the line point is on

Add a faint background highlight to the line we’re editing.

(add-hook 'prog-mode-hook #'hl-line-mode)
(add-hook 'text-mode-hook #'hl-line-mode)
(add-hook 'org-mode-hook #'hl-line-mode)

Always turn on flymake in prog mode

(add-hook 'prog-mode-hook #'flymake-mode)

Another, related mode is flyspell-prog-mode, which is just checks spelling in comments and strings.

(add-hook 'prog-mode-hook #'flyspell-prog-mode)

Automatically create matching parentheses in programming modes

(add-hook 'prog-mode-hook (lambda () (electric-pair-mode t)))
(add-hook 'prog-mode-hook (lambda () (show-paren-mode t)))

Shorten yes/no prompts to y/n

(setq use-short-answers t)

Delete whitespace on save

I would also like to have a good-looking display for trailing whitespace and leading tabs like in my Neovim setup, but it has proven challenging to just narrow down to those two faces. In the interim, I toggle M-x whitespace-mode to check for mixed tabs, spaces, and line endings.

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

Killing buffers with a running process

Typically, Emacs will ask you to confirm before killing a buffer that has a running process, such as with run-python, a *shell* buffer, or a *compilation* buffer.

(delete 'process-kill-buffer-query-function kill-buffer-query-functions)

Don’t wrap lines

I much prefer having long lines simply spill off to the right of the screen than having them wrap around onto the next line, except in the case where I’d like to see wrapped line content, like in one of the shell modes.

(setq-default truncate-lines t)
(add-hook 'eshell-mode-hook (lambda () (setq-local truncate-lines nil)))
(add-hook 'shell-mode-hook (lambda () (setq-local truncate-lines nil)))

Relative line numbers

For programming and prose/writing modes. For large, folded files (like this README), I had an issue where the relative line numbers wouldn’t line up, and looked like this:

https://github.com/renzmann/.emacs.d/blob/d05f019b5a4e0dcac1048e3ecfe995655610957f/img/bad-line-numbers.gif

Super distracting. Setting display-line-numbers-width to 3 so that the thousands place lines up looks pretty darn good no matter how many lines are in the document. It’s very infrequent that I’d have to open up a file in the 10’s of thousands of lines, so this is working great so far.

(defun renz/display-relative-lines ()
  (setq display-line-numbers-width 3)
  (setq display-line-numbers 'relative))

(add-hook 'prog-mode-hook #'renz/display-relative-lines)
(add-hook 'yaml-mode-hook #'renz/display-relative-lines)
(add-hook 'text-mode-hook #'renz/display-relative-lines)

The result:

https://github.com/renzmann/.emacs.d/blob/e7298cf8188bc08a643feb797f1108ad0187cac6/img/good-line-numbers.gif

Delete region when we yank on top of it

I just think that’s a funny sentence. Normally when yanking text with an active region, the region will remain and the yanked text is just inserted at point. I prefer the modern word processor behavior of replacing the selected text with the yanked content.

(delete-selection-mode t)

Enable mouse in terminal/TTY

(xterm-mouse-mode 1)

Compilation

As new text appears, the default behavior is for it to spill off the bottom, unless we manually scroll to the end of the buffer. Instead, I prefer the window to automatically scroll along with text as it appears, stopping at the first error that appears.

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

Tool bar

I usually leave the tool bar disabled

(tool-bar-mode -1)

The menu bar, on the other hand (menu-bar-mode), is very handy, and I only disable it on Windows, where it looks hideous if I’m running in dark mode.

(when (renz/windowsp)
  (menu-bar-mode -1))

For newcomers to Emacs, I would strongly discourage disabling the menu bar, as it is the most straightforward way to discover Emacs’ most useful features.

Ignore risky .dir-locals.el

From an Emacs stackexchange answer.

(advice-add 'risky-local-variable-p :override #'ignore)

grep and find

When ripgrep is installed, I set it as the default grep tool. For project-wide grep searching, I use one of these combinations:

  1. C-x p x (project-execute-extended-command), followed by M-x find-grep
  2. C-x p D (project-dired), followed by C-c g, bound to find-grep below
(use-package grep
  :bind ("C-c g" . grep-find)
  :config
  (when (and (executable-find "rg") (renz/windowsp))
    (grep-apply-setting 'grep-find-command
                        '("rg --vimgrep --color never --ignore-case  ." . 42))))

It would be nice to do something similar for fd over find, but find . {} is hardcoded into find-dired, so unfortunately getting something like this to work won’t be quite as simple:

(when (executable-find "fd")
  (setq find-program "fd")
  (setq find-ls-option nil))

At least on Windows we can attempt to point to a GnuWin version of find, though. At some point I should probably parameterize this better, but right now I’m always putting Gnu utilities under Program Files (x86)\GnuWin32 on my Windows machines.

(when-let ((on-windows (renz/windowsp))
           (prog-files (getenv "PROGRAMFILES(x86)"))
           (find-prg (expand-file-name "GnuWin32/bin/find.exe" prog-files))
           (find-exists (executable-find find-prg)))
  (setq find-program "C:\\\"Program Files (x86)\"\\GnuWin32\\bin\\find.exe"))

Confirm when exiting Emacs

It’s very annoying when I’m working and suddenly I meant to do C-c C-x, but instead hit C-x C-c. This helps prevent that.

(setq confirm-kill-emacs 'yes-or-no-p)

Smooth scrolling

Emacs 29 introduced smooth, pixel-level scrolling, which removes much of the “jumpiness” you see when scrolling past images.

(if (version< emacs-version "29.0")
    (pixel-scroll-mode)
  (pixel-scroll-precision-mode 1)
  (setq pixel-scroll-precision-large-scroll-height 35.0))

Spellchecking

On macOS and linux I typically use aspell, given how easy it is to install. For Windows, I’ll set up hunspell, which I install from the hunspell-binary repo. After installing the hunspell binary, it requires installing a dictionary and affix file to the installation directory:

curl -o en_US.dic https://cgit.freedesktop.org/libreoffice/dictionaries/plain/en/en_US.dic?id=a4473e06b56bfe35187e302754f6baaa8d75e54f
curl -o en_US.aff https://cgit.freedesktop.org/libreoffice/dictionaries/plain/en/en_US.aff?id=a4473e06b56bfe35187e302754f6baaa8d75e54f

Then move these files to wherever hunspell is. For instance, C:\Program Files\Hunspell.

(cond ((executable-find "aspell")
       (setq ispell-program-name "aspell"
             ispell-really-aspell t))
      ((executable-find "hunspell")
       (setq ispell-program-name "hunspell"
             ispell-really-hunspell t)))

Also on windows, you’ll need to set up two things in your “System Environment Variables,” if you are able to edit it. Assuming you installed Hunspell to %PROGRAMFILES%\Hunspell, and you moved the .dic and .aff files to the same directory, you’d set up your variables like this:

  1. Add %PROGRAMFILES%\Hunspell\bin to your user PATH
  2. Add a new variable DICPATH under “User variables” with value %PROGRAMFILES%\Hunspell

If you can’t edit your System’s environment variables through the GUI, say, because you’re on a VM you don’t administer, then you’ll have to set these two environment variables through your powershell or CMD profiles.

Backup and auto-save files

Keep all backup files in a temporary folder. At the moment I have some “file not found” errors popping up during auto-save on Windows. Once I debug that, I’ll uncomment the second part.

(setq backup-directory-alist `(("." . ,(concat user-emacs-directory "backups")))
      backup-by-copying t)

Enable narrow-to-region

narrow-to-region restricts editing in this buffer to the current region. The rest of the text becomes temporarily invisible and untouchable but is not deleted; if you save the buffer in a file, the invisible text is included in the file. C-x n w makes all visible again.

(put 'narrow-to-region 'disabled nil)

Enable up/downcase-region

Allows us to convert entire regions to upper or lower case.

(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)

Mark rings and registers: bigger, faster, stronger

16 is the default number of marks stored on the global and local mark rings is 16. I hop around much more than 16 times as I’m editing, so I expand this a bit.

(setq-default mark-ring-max 32)
(setq global-mark-ring-max 32)

And, because I always forget it, to pop a global mark you use C-x C-<SPC>. The local version, C-u C-<SPC> will only pop marks from the current buffer. So the C-x C-<SPC> version is much closer to how Vim’s jump stack works.

A handy “bookmark” system (aside from actual bookmarks) is to set common buffers and files to registers pre-emptively.

(set-register ?S '(buffer . "*scratch*"))
(set-register ?I `(file . ,(expand-file-name "README.org" user-emacs-directory)))
(set-register ?B `(file . "~/.bashrc"))

The default keybinding for jump-to-register is C-x r j R, where R is the name of the register. My own personal convention here is to use lower-case letter for interactive session bookmarks that will be lost between sessions, and upper-case letters for ones I’ve set permanently here.

Before I was aware of this feature I had created my own jump-to-X style functions, but this is much better! You even get a handy pop-up if you wait a second after typing C-x r j to see all the available registers.

eldoc

I find it very distracting when eldoc suddenly pops up and consumes a large part of the screen for docstrings in python.

(setq eldoc-echo-area-use-multiline-p nil)

imenu

(use-package imenu
  :config
  (setq imenu-auto-rescan t
        org-imenu-depth 3))

dabbrev

Skip over image and PDF buffers when collecting candidates for dynamic abbreviation.

(use-package dabbrev
  :custom
  (dabbrev-ignored-buffer-regexps '("\\.\\(?:pdf\\|jpe?g\\|png\\)\\'")))

Hippie-expand is purported to be a better version of dabbrev.

(use-package hippie-exp
  :config
  (global-set-key [remap dabbrev-expand] 'hippie-expand)
  (delete 'try-expand-line hippie-expand-try-functions-list)
  (delete 'try-complete-lisp-symbol-partially hippie-expand-try-functions-list)
  (delete 'try-complete-lisp-symbol hippie-expand-try-functions-list))

dired

By default, dired uses bytes instead of “K”, “Mb”, or “G” for file sizes. I also have it hide the mode, size, and owner of each file by default.

(use-package dired
  :hook (dired-mode . dired-hide-details-mode)
  :config
  (setq dired-listing-switches "-alFh")
  (setq dired-dwim-target t))

Also enabled above is Do-What-I-Mean (DWIM) copying. This is for when two dired windows are open, and we want to copy something from one location to the other. By enabling dired-dwim-target, it auto-populates the minibuffer with the other dired window’s path when issuing a copy command with C.

eww - search engine and browser

Ecosia requires JavaScript, unfortunately.

(use-package eww
  :config (setq eww-search-prefix "https://duckduckgo.com/html/?q="))

Language Server Protocol (LSP) with eglot

As of version 29, eglot (Emacs polyGLOT) is bundled with Emacs. It provides Emacs with the client side configuration for the language server protocol.

(use-package eglot
  :bind (("C-c l c" . eglot-reconnect)
         ("C-c l d" . flymake-show-buffer-diagnostics)
         ("C-c l f f" . eglot-format)
         ("C-c l f b" . eglot-format-buffer)
         ("C-c l l" . eglot)
         ("C-c l r n" . eglot-rename)
         ("C-c l s" . eglot-shutdown)
         ("C-c l i" . eglot-inlay-hints-mode)))

To have eglot always start up for a python buffer, we would tangle this line into init.el. However, this can cause a significant loading delay over Tramp, and I would prefer snappy, simple access with LSP provided on an as-needed basis.

(add-hook 'python-mode-hook 'eglot-ensure)

Side show: semantic-mode

For a while, it looks like Emacs was trying out something called semantic-mode, which looks a lot like a precursor to what we now know as the Language Server Protocol. Enabling it was done through adding the semantic-mode hook to your language’s major mode hook:

(add-hook 'python-mode-hook 'semantic-mode)

Shell commands

The Async command buffer’s default behavior is to print ^M characters (the carriage return) instead of actually clearing text. This is problematic for spinners and progress bars, so I have a little hack to work around that.

(defun renz/async-shell-command-filter-hook ()
  "Filter async shell command output via `comint-output-filter'."
  (when (equal (buffer-name (current-buffer)) "*Async Shell Command*")
    ;; When `comint-output-filter' is non-nil, the carriage return characters ^M
    ;; are displayed
    (setq-local comint-inhibit-carriage-motion nil)
    (when-let ((proc (get-buffer-process (current-buffer))))
      ;; Attempting a solution found here:
      ;; https://gnu.emacs.help.narkive.com/2PEYGWfM/m-chars-in-async-command-output
      (set-process-filter proc 'comint-output-filter))))

(add-hook 'shell-mode-hook #'renz/async-shell-command-filter-hook)

There might be a better way, but this mostly works for now.

TAGS

Before the whole language server revolution, we had TAGS files for caching the location of symbol definitions. etags comes with Emacs, and combining some clever use of find with it can render a pretty good symbol search experience. To generate the TAGS file, I usually have a TAGS recipe that looks something similar to this in each project’s Makefile:

find . -type d -name ".venv" -prune \
    -o -type d -name ".ipynb_checkpoints" -prune \
    -o -type d -name ".node_modules" -prune \
    -o -type d -name "elpa" -prune \
    -o -type f -name "*.py" -print \
    -o -type f -name "*.sql" -print \
    -o -type f -name "*.el" -print \
    | etags -

Then, M-x project-compile RET make TAGS builds a tags table. At which point, I can use M-x visit-tags-table RET RET to build a list of symbols I can navigate to with completion. The built-in xref works with the tags table, so commands like xref-find-definitions will offer jump-to-definition behavior in tandem with completing-read.

‘Uniquify’ using slash instead of brackets

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

Save our place in files when we re-visit them

(save-place-mode 1)

Keep existing clipboard text in kill ring

(setq save-interprogram-paste-before-kill t)

More extensive apropos search

(setq apropos-do-all t)

Disable implied frame resizing

(setq frame-inhibit-implied-resize t)

Use one frame for ediff

(setq ediff-window-setup-function 'ediff-setup-windows-plain)

Prefer newer files on load

(setq load-prefer-newer t)

Keybindings

Expanded/better defaults

(global-set-key (kbd "C-M-<backspace>") 'backward-kill-sexp)

The next line UNBINDS the suspend-frame keybinding. Accidentally minimizing on the GUI was frustrating as hell, so now I use C-x C-z if I really want to suspend the frame.

(global-set-key (kbd "C-z") #'zap-up-to-char)

ibuffer is a strictly superior, built-in version of its counterpart.

(global-set-key [remap list-buffers] 'ibuffer)

The most common situation where I’m running flymake would be for spelling in prose, or diagnostics from a language server. In either case, I like having next/previous on easy to reach chords.

(use-package flymake
  :bind (:map flymake-mode-map
         ("C-c n" . flymake-goto-next-error)
         ("C-c p" . flymake-goto-prev-error)))

C-c bindings

Emacs has some standards about where user-configured keys should go; C-c <letter> is always free for users. It may seem like overkill how I set a header for each possible C-c combination, but it’s incredibly handy when I want to jump directly to one of these headings while in another buffer. See e.g. org-goto, which allows me to narrow in on a particular key I’d like to bind by leveraging completing-read. If a C-c <letter> combination is missing as a header, then I’m probably using it in a :bind statement with use-package somewhere else.

C-c b build / compile

(global-set-key (kbd "C-c b") #'compile)
(global-set-key (kbd "C-c B") #'recompile)

C-c c Insert current dir/file at point

(defun renz/insert-current-dir ()
  "Insert the current `default-directory' at point."
  (interactive)
  (insert default-directory))

(defun renz/insert-current-file ()
  "Insert the current buffer's full file name at point."
  (interactive)
  ;; https://unix.stackexchange.com/a/45381
  (insert (buffer-file-name (window-buffer (minibuffer-selected-window)))))

(global-set-key (kbd "C-c c d") #'renz/insert-current-dir)
(global-set-key (kbd "C-c c f") #'renz/insert-current-file)

C-c d delete pairs of surrounding characters

(global-set-key (kbd "C-c d") #'delete-pair)
(setq delete-pair-blink-delay 0.0)

C-c i browse url of buffer

(global-set-key (kbd "C-c i") #'browse-url-of-buffer)

C-c j Toggle window split

Toggling windows from vertical to horizontal splits and vice-versa.

(defun toggle-window-split ()
  "Switch between horizontal and vertical split window layout."
  (interactive)
  (if (= (count-windows) 2)
      (let* ((this-win-buffer (window-buffer))
             (next-win-buffer (window-buffer (next-window)))
             (this-win-edges (window-edges (selected-window)))
             (next-win-edges (window-edges (next-window)))
             (this-win-2nd (not (and (<= (car this-win-edges)
                                         (car next-win-edges))
                                     (<= (cadr this-win-edges)
                                         (cadr next-win-edges)))))
             (splitter
              (if (= (car this-win-edges)
                     (car (window-edges (next-window))))
                  'split-window-horizontally
                'split-window-vertically)))
        (delete-other-windows)
        (let ((first-win (selected-window)))
          (funcall splitter)
          (if this-win-2nd (other-window 1))
          (set-window-buffer (selected-window) this-win-buffer)
          (set-window-buffer (next-window) next-win-buffer)
          (select-window first-win)
          (if this-win-2nd (other-window 1))))))

(global-set-key (kbd "C-c j") #'toggle-window-split)

C-c k kill all but one space

I used to bind this to just-one-space before I knew about M-SPC (cycle-spacing). Now I use it to bury buffers instead of killing them.

(global-set-key (kbd "C-c k") #'bury-buffer)

C-c q replace regexp

(global-set-key (kbd "C-c q") #'replace-regexp)

C-c r find recent files

(global-set-key (kbd "C-c r") #'renz/find-recent-file)

C-c t visit tags table

(global-set-key (kbd "C-c t") #'visit-tags-table)

C-c s shell

(global-set-key (kbd "C-c s s") #'shell)
(global-set-key (kbd "C-c s e") #'eshell)
(global-set-key (kbd "C-c s t") #'vterm)

C-c u open URL at point in browser

(global-set-key (kbd "C-c u") #'browse-url-at-point)

C-c v faster git-commit

(defun renz/git-commit ()
  (interactive)
  (vc-next-action nil)
  (log-edit-show-diff)
  (other-window 1))

(global-set-key (kbd "C-c v") #'renz/git-commit)

C-c w whitespace mode

(global-set-key (kbd "C-c w") #'whitespace-mode)

C-c x misc. “execute” commands

(global-set-key (kbd "C-c x r") #'restart-emacs)

C-c Other bindings

(global-set-key (kbd "C-c <DEL>") #'backward-kill-sexp)  ;; TTY-frindly
(global-set-key (kbd "C-c <SPC>") #'mark-sexp)  ;; TTY-friendly

F5-F9

Like the C-c <letter> bindings, these are reserved for users. In practice, even though there are few of these keys, I tend to forget which is which. So I wind up using things bound to my C-c keymaps instead. The C-c kyes from a more natural, nested language in my head, so it feels more like I’m “speaking Emacs” that way.

Super bindings

(global-set-key (kbd "s-p") #'project-switch-project)

Text Completion

Emacs offers incredible depth and freedom when configuring methods to automatically complete text. There are actually two things that “autocompletion” can refer to in Emacs:

  1. Minibuffer completion
  2. Completion at point

Emacs on its own does not have a nice pop-up-menu like Vim for completing text at point. For both the minibuffer and completion-at-point it uses a special buffer called *Completions*, from which we can see (and optionally select) a completion from potential candidates. Before we get to tweak those settings, though, we first need to oil the engine with an enhanced completion style

Completion style

For both the minibuffer and completion-at-point, I use the same completion style. Completion style is the method of assigning completion candidates to a given input string. flex is the built-in “fuzzy” completion style, familiar to us from symbol completion in IDEs and VSCode’s command palette. basic functions much like your default TAB-complete at a Bash shell.

(use-package orderless
  :custom
  (completion-styles '(orderless flex basic))
  (completion-category-overrides '((file (styles basic partial-completion)))))

Nicer Display and Behavior of *Completions*

With the completion style set, we now have to configure the interface for displaying candidates as we type. First, I want candidates displayed as a single, vertical list.

(setq completions-format 'one-column)

Also, when using the built-in completion-at-point, the *Completions* buffer can sometimes take up the whole screen when there are a lot of candidates.

(unless (version< emacs-version "29.0")
  (setq completions-max-height 15))

Some time ago, Prot wrote a package called MCT (Minibuffer and Completions in Tandem) that enhanced the default minibuffer and *Completions* buffer behavior to act more like what we expect of a modern editor’s auto-complete. He discontinued development of that project once it became clear that Emacs 29 was going to include similar behavior as a configurable option. These are the options in question.

(unless (version< emacs-version "29.0")
  (setq completion-auto-help 'always
        completion-auto-select 'second-tab
        completion-show-help nil
        completions-sort nil
        completions-header-format nil))

Completion in the minibuffer and at point

By default, Emacs uses M-TAB, or the equivalent C-M-i for completion-at-point. I’d much prefer to use the easier and more intuitive TAB.

(setq tab-always-indent 'complete)

Something I once tried is to use icomplete along with icomplete-in-buffer to get something like a little window that updates as I type. It seems a little wonky, since TAB-completion will still cause the ∗Completions∗ buffer to pop up, even while Icomplete is active, unless we set completion-auto-help to lazy; and even then it will still come up on the second TAB press.

(setq icomplete-in-buffer t)
(setq icomplete-prospects-height 10)
(icomplete-vertical-mode t)

In the case that we need to enter a new file name, but fido is still showing a completion candidate, you have to use C-d to refuse completion and take whatever is currently in the prompt. For instance, if we are editing a file hello.py, and then use C-x C-f hell.py, the minibuffer will complete hell.py into hello.py if we use RET, and will open a new buffer for hell.py if we use C-d.

Corfu

For in-buffer pop-up completion.

(use-package corfu
  :custom
  (corfu-auto nil)
  (corfu-auto-delay 0.1)
  (corfu-quit-no-match 'separator)
  (global-corfu-modes '((not shell-mode) (not eshell-mode) t))
  :config
  (global-corfu-mode))

Vertico

For fast minibuffer completion.

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

Marginalia

For more details when displaying candidates with vertico

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

Language-specific major modes

Shell (Bash, sh, …)

(defun renz/sh-indentation ()
  ;; (setq indent-tabs-mode t)
  (setq tab-width 8))

(add-hook 'sh-mode-hook #'renz/sh-indentation)
(add-hook 'bash-ts-mode-hook #'renz/sh-indentation)

HTML

This changes the behavior of a few commonly-used tags in web pages that I write.

(use-package sgml-mode
  :defer t
  :custom
  (electric-pair-local-mode nil)
  :config
  (let* ((p-tag-old (assoc "p" html-tag-alist))
         ;; Close the <p> tag and open on a new line.
         (p-tag-new `("p" \n ,(cdr (cdr p-tag-old)))))
    (add-to-list 'html-tag-alist p-tag-new)
    ;; Close the <code> tag and stay inline.
    (add-to-list 'html-tag-alist '("code"))))

CSS

(setq css-indent-offset 2)

For validation, grab css-validator.jar and execute it with java:

java -jar ~/.local/jars/css-validator.jar file:///home/me/my/site/index.html

Org-mode

(setq renz/org-home "~/.emacs.d/org/")
(put 'org-publish-project-alist 'safe-local-variable #'listp)
(put 'org-html-validation-link 'safe-local-variable #'symbolp)
(put 'org-html-head-include-scripts 'safe-local-variable #'symbolp)
(put 'org-html-head-include-default-style 'safe-local-variable #'symbolp)
(put 'org-html-head 'safe-local-variable #'stringp)

org-mode provides org-babel-tangle-jump-to-org, which jumps back to an Org source file from within the tangled code. renz/org-babel-tangle-jump-to-src, defined below, does the opposite - given the Org source file and point inside a src block, it jumps to the location of the tangled code. Provided by a helpful stackoverflow answer.

(defun renz/org-babel-tangle-jump-to-src ()
  "The opposite of `org-babel-tangle-jump-to-org'.
Jumps to an Org src block from tangled code."
  (interactive)
  (if (org-in-block-p)
      (let* ((header (car (org-babel-tangle-single-block 1 'only-this-block)))
             (tangle (car header))
             (lang (caadr header))
             (buffer (nth 2 (cadr header)))
             (org-id (nth 3 (cadr header)))
             (source-name (nth 4 (cadr header)))
             (search-comment (org-fill-template
                              org-babel-tangle-comment-format-beg
                              `(("link" . ,org-id) ("source-name" . ,source-name))))
             (file (expand-file-name
                    (org-babel-effective-tangled-filename buffer lang tangle))))
        (if (not (file-exists-p file))
            (message "File does not exist. 'org-babel-tangle' first to create file.")
          (find-file file)
          (beginning-of-buffer)
          (search-forward search-comment)))
    (message "Cannot jump to tangled file because point is not at org src block.")))

Now we configure org-mode itself. For a while I was trying (setq org-startup-indented t) to get indentation under each header, but this was interfering with the beautification features from org-modern. Preferring the latter over the former, I’ve removed the org-startup-indented call.

(defun renz/list-files-with-absolute-path (directory)
  "Return a list of files in DIRECTORY with their absolute paths."
  (cl-remove-if-not #'file-regular-p (directory-files directory t ".*\.org$")))

(use-package org
  :hook
  ((org-mode . (lambda () (progn
                            (add-hook 'after-save-hook #'org-babel-tangle :append :local)
                            (add-hook 'org-babel-after-execute-hook #'renz/display-ansi-colors)
                            (setq indent-tabs-mode nil)))))

  :init
  (defun renz/jump-org ()
    "Prompt for an org file in my emacs directory, then go there."
    (interactive)
    (renz/--jump-section renz/org-home "Org files: " ".*\.org$"))

  :bind
  (("C-c o a" . org-agenda)
   ("C-c o b d" . org-babel-detangle)
   ("C-c o b o" . org-babel-tangle-jump-to-org)
   ("C-c o b s" . renz/org-babel-tangle-jump-to-src)
   ("C-c o k" . org-babel-remove-result)
   ("C-c o o" . renz/jump-org)
   ("C-c o y" . ox-clip-image-to-clipboard))

  :custom
  (org-image-actual-width nil "Enable resizing of images")
  (org-agenda-files (renz/list-files-with-absolute-path renz/org-home) "Sources for Org agenda view")
  (org-html-htmlize-output-type nil "See C-h f org-html-htmlize-output-type")
  (org-confirm-babel-evaluate nil "Don't ask for confirmation when executing src blocks")
  (org-goto-interface 'outline-path-completion "Use completing-read for org-goto (C-c C-j, nicer than imenu)")
  (org-outline-path-complete-in-steps nil "Flatten the outline path, instead of completing hierarchically")

  :config
  (add-to-list 'org-modules 'org-tempo)
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (python . t)
     (sql . t)
     (shell . t)
     (R . t)
     ;; (fortran . t)
     ;; (julia . t)
     ;; (jupyter . t)
     ;; (scheme . t)
     ;; (haskell . t)
     (lisp . t)
     ;; (clojure . t)
     ;; (C . t)
     ;; (org . t)
     ;; (gnuplot . t)
     ;; (awk . t)
     ;; (latex . t)
     )))

SQL

DDL is SQL

(add-to-list 'auto-mode-alist '("\\.ddl\\'" . sql-mode))
(add-to-list 'auto-mode-alist '("\\.bql\\'" . sql-mode))

Indentation

Vanilla Emacs doesn’t offer a lot (read: nothing) in terms of making SQL code pretty. I tend to format SQL like this:

SELECT
    whatever,
    thing
FROM
    wherever AS w
    JOIN the_other AS t ON w.id = t.id
GROUP BY
    whatever

The configuration of sql-indent below achieves that nicely when using RET and TAB for formatting.

(defun renz/sql-mode-hook ()
  (setq tab-width 4))

(defvar renz/sql-indentation-offsets-alist
  '((syntax-error sqlind-report-sytax-error)
    (in-string sqlind-report-runaway-string)
    (comment-continuation sqlind-indent-comment-continuation)
    (comment-start sqlind-indent-comment-start)
    (toplevel 0)
    (in-block +)
    (in-begin-block +)
    (block-start 0)
    (block-end 0)
    (declare-statement +)
    (package ++)
    (package-body 0)
    (create-statement +)
    (defun-start +)
    (labeled-statement-start 0)
    (statement-continuation +)
    (nested-statement-open sqlind-use-anchor-indentation +)
    (nested-statement-continuation sqlind-use-previous-line-indentation)
    (nested-statement-close sqlind-use-anchor-indentation)
    (with-clause sqlind-use-anchor-indentation)
    (with-clause-cte +)
    (with-clause-cte-cont ++)
    (case-clause 0)
    (case-clause-item sqlind-use-anchor-indentation +)
    (case-clause-item-cont sqlind-right-justify-clause)
    (select-clause 0)
    (select-column sqlind-indent-select-column)
    (select-column-continuation sqlind-indent-select-column +)
    (select-join-condition ++)
    (select-table sqlind-indent-select-table)
    (select-table-continuation sqlind-indent-select-table +)
    (in-select-clause sqlind-lineup-to-clause-end sqlind-right-justify-logical-operator)
    (insert-clause 0)
    (in-insert-clause sqlind-lineup-to-clause-end sqlind-right-justify-logical-operator)
    (delete-clause 0)
    (in-delete-clause sqlind-lineup-to-clause-end sqlind-right-justify-logical-operator)
    (update-clause 0)
    (in-update-clause sqlind-lineup-to-clause-end sqlind-right-justify-logical-operator)))

(defun renz/sql-indentation-offsets ()
  (setq sqlind-indentation-offsets-alist
        renz/sql-indentation-offsets-alist)
  (setq sqlind-basic-offset 4))

(use-package sql-indent
  :hook (sqlind-minor-mode . renz/sql-indentation-offsets))

(use-package sql-mode
  :hook ((sql-mode . renz/sql-mode-hook)
         (sql-mode . sqlup-mode)
         (sql-mode . sqlind-minor-mode)))

Interactive hive2 mode

This “hive2” package came from the days where I was working on an on-prem system that used hive2 as the main command-line interface to Hive. I don’t use this much now, but it’s a good reference for implementing a plug-in to a new interactive SQL CLI.

(use-package hive2
  :load-path "site-lisp/"
  :demand t
  :mode ("\\.hql" . sql-mode))

Interactive bq shell

The SQL interactive commands are looking for a single executable file, so let’s set that up somewhere common, like ~/.local/bin/bq-shell.

#!/usr/bin/env sh
bq shell "$@"

Also, we don’t want to use “legacy SQL” in our queries, which requires us to configure the bq query statically in a ~/.bigqueryrc file, according to the Google issue tracker.

[query]
--use_legacy_sql=false

Then enable the BQ product.

(use-package bq
  :load-path "site-lisp"
  :demand t)

BigQuery sql Blocks in Org-Babel

Advising org-babel-execute:sql in this way allows me to use #+begin_src sql :engine bq :results raw blocks in org-babel and execute them with C-c C-c. More commonly, though, I set #+PROPERTY: header-args:sql :engine bq :results raw at the top of the document so that I can just mark a src block as sql and be done with it.

(defun org-babel-execute:bq (orig-fun body params)
  (if (string-equal-ignore-case (cdr (assq :engine params)) "bq")
      (json-to-org-table-parse-json-string
       (org-babel-execute:shell (concat "bq query --format=json --nouse_legacy_sql '" body "'")
                                params))
    (org-babel-execute:sql body params)))

(advice-add 'org-babel-execute:sql :around #'org-babel-execute:bq)

This also typically requires #+OPTIONS: ^:nil at the top of the Org document to stop underscores from messing up how column names are displayed.

Python

Pipfiles are TOML

(add-to-list 'auto-mode-alist '("Pipfile" . toml-ts-mode))

Ignore .venv in VC operations

(add-to-list 'vc-directory-exclusion-list ".venv")

Flatten items in imenu

The default behavior in large Python buffers is to nest symbols, so after using C-c C-j (imenu), you first have to complete what kind of symbol you’re looking for, such as a “Function”, “Class”, or “Variable”, then complete the symbol itself. I’d much rather just search for the symbol to begin with, and by using this flat index it will show me the symbol’s type when I go to complete it.

(add-hook 'python-mode-hook
          (lambda () (setq-local imenu-create-index-function
                                 'python-imenu-create-flat-index)))

(add-hook 'python-ts-mode-hook
          (lambda () (setq-local imenu-create-index-function
                                 'python-imenu-treesit-create-flat-index)))

Interactively setting the virtual environment for pyrightconfig.json

The most consistent way to get eglot to properly configure the python virtual environment with pyright is to have a static file at the root of the project, called pyrightconfig.json. I wrote a short plugin that allows me to select a directory using completing-read and have Emacs write the content of pyrightconfig.json based on what I selected, in the appropriate directory.

(defun pyrightconfig-write (virtualenv)
  "Write a `pyrightconfig.json' file at the Git root of a project
with `venvPath' and `venv' set to the absolute path of
`virtualenv'.  When run interactively, prompts for a directory to
select."
  (interactive "DEnv: ")
  ;; Naming convention for venvPath matches the field for pyrightconfig.json
  (let* ((venv-dir (tramp-file-local-name (file-truename virtualenv)))
         (venv-file-name (directory-file-name venv-dir))
         (venvPath (file-name-directory venv-file-name))
         (venv (file-name-base venv-file-name))
         (base-dir (vc-git-root default-directory))
         (out-file (expand-file-name "pyrightconfig.json" base-dir))
         (out-contents (json-encode (list :venvPath venvPath :venv venv))))
    (with-temp-file out-file (insert out-contents))
    (message (concat "Configured `" out-file "` to use environment `" venv-dir))))

Configuring pyright this way rather than “activating” an environment through Emacs (ala pythonic-activate or similar) means we can be running the language server in more than one project at a time, each pointing to its respective virtual environment.

Pyright error links in *compilation*

The M-x compile feature does not recognize or parse pyright error messages out of the box, so I add that support myself. Here’s an example error message:

/home/robb/tmp/errors.py/
  /home/robb/tmp/errors.py:1:1 - error: "foo" is not defined (reportUndefinedVariable)
  /home/robb/tmp/errors.py:1:1 - warning: Expression value is unused (reportUnusedExpression)
  /home/robb/tmp/errors.py:4:12 - error: Operator "+" not supported for types "str" and "Literal[1]"
    Operator "+" not supported for types "str" and "Literal[1]" (reportGeneralTypeIssues)
2 errors, 1 warning, 0 informations

To get the basic M-g M-n and M-g M-p navigation working, we just need a regex to parse file name, line, and column number.

(with-eval-after-load 'compile
  (add-to-list 'compilation-error-regexp-alist-alist
               '(pyright "^[[:blank:]]+\\(.+\\):\\([0-9]+\\):\\([0-9]+\\).*$" 1 2 3))
  (add-to-list 'compilation-error-regexp-alist 'pyright))

It would be nice if we could also capture the \\(error\\|warning\\) part as “KIND”, but I’m struggling to get it working.

Python check with “ruff”

Another nice vanilla feature of python-mode is M-x python-check, which runs a pre-specified linter. Setting that to mypy or pyright if either of those programs exist is a small time saver.

(use-package python
  :config
  (require 'eglot)
  (setq python-check-command "ruff check")
  (add-hook 'python-mode-hook #'flymake-mode)
  (add-hook 'python-ts-mode-hook #'flymake-mode))

I do not run ruff format on save for one reason: If I am patching code in a project that is not currently formatted using the black (ruff format) style, I do not want to introduce formatting changes alongside any logic changes I’m suggesting. I’d rather issue a formatting command through a project-level shell command or python-check on-demand, rather than disable formatting after realizing it was a problem.

Fix Microsoft Windows Issues

At one point, I ran into something similar to this elpy issue on Windows. The culprit was “App Execution Aliases” with python and python3 redirecting to the windows store. Using this fixed it:

winkey -> Manage app execution aliases -> uncheck python and python3

Also on Windows - a pip install of pyreadline3 is required to make tab-completion work at all. It provides the readline import symbol.

Make check command and virtualenv root safe for .dir-locals.el

Virtualenvs require .dir-locals.el to have something like:

((python-mode . ((python-shell-virtualenv-root . "/path/to/my/.venv"))))

However, this only operates on `run-python’ shells. Also, for projects, we need to make sure that setting the virtualenv root is marked as safe.

(put 'python-check-command 'safe-local-variable #'stringp)
(put 'python-shell-virtualenv-root 'safe-local-variable #'stringp)
(put 'python-interpreter 'safe-local-variable #'stringp)

Markdown

When installing markdown through Anaconda, the executable is actually called markdown_py. In case markdown isn’t found, use that instead.

(when (and (not (executable-find "markdown")) (executable-find "markdown_py"))
  (setq markdown-command "markdown_py"))

I make a lot of spelling mistakes as I type…

(add-hook 'markdown-mode-hook 'flyspell-mode)
(add-hook 'markdown-mode-hook 'auto-fill-mode)

And I like to see language syntax highlighting within code fences.

(setq markdown-fontify-code-blocks-natively t)

csv-mode

Handy for viewing data quickly.

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

Eshell

(use-package eshell
  :custom
  (eshell-visual-commands '("make" "vi" "vim" "screen" "tmux" "top" "htop" "less" "more" "lynx" "links" "ncftp" "mutt" "pine" "tin" "trn" "elm"))
  (eshell-visual-subcommands '(("git" "log" "diff" "show")
                               ("micromamba" "install" "update" "upgrade" "create" "run" "self-update")
                               ("mamba" "install" "update" "upgrade")
                               ("poetry" "install" "update" "upgrade")
                               ("docker" "build")
                               ("uv" "pip"))))

Tool configuration

These are tweaks for third party packages.

treesit-auto: Automatically Using TreeSitter Modes

I’ve posted this to GitHub and MELPA as treesit-auto.

(use-package treesit-auto
  :custom
  (treesit-auto-install 'prompt)
  (treesit-auto-langs '(awk bash c css go html javascript json make markdown r ruby rust toml typescript yaml))
  :config
  (treesit-auto-add-to-auto-mode-alist 'all)
  (global-treesit-auto-mode))

Before it was published to MELPA, I used a git subtree to manage the plugin. This is a pretty useful technique, so I keep these two one-liners around in case I need to reference or copy them. To get a copy of something as a subtree, I use this:

git subtree add -P site-lisp/treesit-auto [email protected]:renzmann/treesit-auto main --squash

Fetching updates is a similar command.

git subtree pull -P site-lisp/treesit-auto [email protected]:renzmann/treesit-auto main --squash

You can get pre-compiled grammars here, as well: emacs-tree-sitter/tree-sitter-langs @ GitHub

pyvenv

The case for including this package is incredibly strong after trying my hand at getting Python virtual environments working in a vanilla way that’s platform independent. In the end, we’d just wind up re-creating pyvenv. The main bug in python.el is that it assumes that the interpreter lives under .venv/Scripts/ if we’re on Windows, which is not true if the environment was created with MinGW (it uses the standard bin/ directory). pyvenv, on the other hand, simply checks what folders exists, and uses the first one it finds, so it works equally well on macOS, Linux, Windows native, and MinGW. On top of all this, it is only ~600 lines of code, so it is a very small dependency to include.

(use-package pyvenv
  ;; Overrides `mark-page'
  :bind (("C-x p a" . pyvenv-activate)
         ("C-x p u" . pyvenv-deactivate))
  :config
  (put 'pyvenv-mode 'safe-local-variable #'stringp)
  (pyvenv-tracking-mode 1)
  (pyvenv-mode 1))

direnv Managing project environment variables

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

vterm Terminal emulation

Binding the terminal to an unused C-x allows for things like C-x 4 p t to open the terminal in a new split.

(use-package vterm
  :bind ("C-x p t" . vterm)
  :custom (vterm-tramp-shells '(("docker" "/bin/bash"))))

Cloud stuff

(defun renz/glogin ()
  "Log in to GCP"
  (interactive)
  (shell-command "gcloud auth login --update-adc"))

Wishlist

  • Tramp is hanging, but not when using emacs -q. Prune any docker stuff from my config
  • Think about efficient editing motions with a cheatsheet that translates from Vim
  • Remove my keybinding section and put it under a use-package emacs block
  • fzf-like general completion for paths and files. project-switch-project is pretty close, but only good for known projects
  • When running BigQuery from a *compilation* buffer, it would be nice if I could get error markers to jump directly to the issue
  • Make corfu less invasive. Common issues:
    • Enter accepts something when I wanted to go to a new line
    • pop-up blocking what I wanted
  • History of Async commands + results just for today. Much like having a terminal session that I can scroll back through. Sometimes I just want to see what I did earlier - a big benefit of having a long-running Bash session in the terminal.
    • Write history to a file in ~/.emacs.d
    • Have a rolling “max size”
    • Maybe also do this for *compilation*?
  • The __PYTHON_EL_eval_file thing is still really fucking annoying
  • Elisp: advise M-& to insert the command run in buffer name

Completed from wishlist

  • A good tmux-like workflow for running multiple async compilation jobs, with an easy way to kill and restart each
    • Best way to do this is to use C-x p M-&, or C-x p c to start an async or compilation job, then rename the buffer using C-x x r. When visiting this buffer, g will kill and restart the job.
  • Is an LSP-less python experience good or desirable?
    • Fuck no.
  • Eshell commands for creating/managing virtualenvs
    • uv has completely annihilated this requirement
    • I just can’t find a good workflow on eshell. The M-! and M-& do everything I would do from eshell, but also track my environment better. Vterm is also very good for actual terminal emulation.
  • How do we use local emacs as EDITOR for tramp connections? Things like default git messages from merges are difficult
    • This just requires a workflow change. Do it from a terminal or ensure we’re always using VC mode.

Footer

Thank you for reading ‘till the end or for being interested on how to end an Emacs package. So that’s it, let’s gracefully finish tangling everything:

(provide 'init.el)
;;; init.el ends here

About

My literate Emacs configuration

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages