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.
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:
- Additional major modes for common file types like Markdown and CSV
- Error message support for
pyright
in a*Compilation*
buffer - Reasonable indentation behavior for SQL files
- Updating buffers automatically if their contents change on disk
- Handling ANSI color escape codes in shell output, compilation, and VC buffers
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.
I steal quite a lot from other, more qualified Emacs community contributors, such as:
- Protesilaos Stavrou
- Ramón Panadestein
- Mickey Petersen
- Daniel Mendler
- Omar Antolín Camarena
- Luca’s Literate Config
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.
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!
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
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:
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))
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))
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))
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.
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.
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))
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.
Hats off to Prot for his wonderful themes.
(load-theme 'modus-vivendi t)
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)
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.
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.
(server-start)
(global-so-long-mode t)
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))
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)
Found this on a System Crafters video.
(savehist-mode 1)
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))))
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)
Soon, I’d like to swap out my hacks above for this more robust package: https://github.com/atomontage/xterm-color/tree/master
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)))
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)
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.
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)
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.
From a helpful stackoverflow answer.
(setq mouse-wheel-tilt-scroll t)
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)
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)
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)
(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)
(add-hook 'prog-mode-hook (lambda () (electric-pair-mode t)))
(add-hook 'prog-mode-hook (lambda () (show-paren-mode t)))
(setq use-short-answers t)
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)
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)
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)))
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:
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:
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)
(xterm-mouse-mode 1)
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)
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.
From an Emacs stackexchange answer.
(advice-add 'risky-local-variable-p :override #'ignore)
When ripgrep
is installed, I set it as the default grep tool. For
project-wide grep searching, I use one of these combinations:
C-x p x
(project-execute-extended-command
), followed byM-x find-grep
C-x p D
(project-dired
), followed byC-c g
, bound tofind-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"))
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)
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))
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:
- Add
%PROGRAMFILES%\Hunspell\bin
to your userPATH
- 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.
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)
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)
Allows us to convert entire regions to upper or lower case.
(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)
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.
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)
(use-package imenu
:config
(setq imenu-auto-rescan t
org-imenu-depth 3))
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))
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
.
Ecosia requires JavaScript, unfortunately.
(use-package eww
:config (setq eww-search-prefix "https://duckduckgo.com/html/?q="))
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)
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)
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.
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
.
(use-package uniquify
:custom (uniquify-buffer-name-style 'forward))
(save-place-mode 1)
(setq save-interprogram-paste-before-kill t)
(setq apropos-do-all t)
(setq frame-inhibit-implied-resize t)
(setq ediff-window-setup-function 'ediff-setup-windows-plain)
(setq load-prefer-newer t)
(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)))
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.
(global-set-key (kbd "C-c b") #'compile)
(global-set-key (kbd "C-c B") #'recompile)
(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)
(global-set-key (kbd "C-c d") #'delete-pair)
(setq delete-pair-blink-delay 0.0)
(global-set-key (kbd "C-c i") #'browse-url-of-buffer)
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)
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)
(global-set-key (kbd "C-c q") #'replace-regexp)
(global-set-key (kbd "C-c r") #'renz/find-recent-file)
(global-set-key (kbd "C-c t") #'visit-tags-table)
(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)
(global-set-key (kbd "C-c u") #'browse-url-at-point)
(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)
(global-set-key (kbd "C-c w") #'whitespace-mode)
(global-set-key (kbd "C-c x r") #'restart-emacs)
(global-set-key (kbd "C-c <DEL>") #'backward-kill-sexp) ;; TTY-frindly
(global-set-key (kbd "C-c <SPC>") #'mark-sexp) ;; TTY-friendly
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.
(global-set-key (kbd "s-p") #'project-switch-project)
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:
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
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)))))
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))
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
.
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))
For fast minibuffer completion.
(use-package vertico
:config
(vertico-mode))
For more details when displaying candidates with vertico
(use-package marginalia
:bind
(:map minibuffer-local-map ("M-A" . marginalia-cycle))
:init
(marginalia-mode))
(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)
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"))))
(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
(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)
)))
(add-to-list 'auto-mode-alist '("\\.ddl\\'" . sql-mode))
(add-to-list 'auto-mode-alist '("\\.bql\\'" . sql-mode))
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)))
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))
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)
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.
(add-to-list 'auto-mode-alist '("Pipfile" . toml-ts-mode))
(add-to-list 'vc-directory-exclusion-list ".venv")
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)))
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.
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.
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.
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.
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)
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)
Handy for viewing data quickly.
(use-package csv-mode
:mode "\\.csv\\'")
(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"))))
These are tweaks for third party packages.
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
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))
(use-package direnv
:config (direnv-mode))
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"))))
(defun renz/glogin ()
"Log in to GCP"
(interactive)
(shell-command "gcloud auth login --update-adc"))
- 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
- 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-&
, orC-x p c
to start an async or compilation job, then rename the buffer usingC-x x r
. When visiting this buffer,g
will kill and restart the job.
- Best way to do this is to use
- 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-!
andM-&
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.
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