Skip to content

Latest commit

 

History

History
1526 lines (1395 loc) · 70.7 KB

init.org

File metadata and controls

1526 lines (1395 loc) · 70.7 KB

-*- eval: (add-hook ‘after-save-hook #’org-babel-tangle nil t) -*-

Libraries and Plumbing

Tangling Hook

There are two main approaches to writing your emacs configuration in org:

org-babel-load-file
This approach is very simple to implement, but has to tangle each file when emacs starts.
org-babel-tangle
This approach stores the tangled file on disk, but you have to remember to re-tangle after each edit.

We can use a local after-save-hook to do the tangling automatically. However, this file-local variable is not considered safe by default, so emacs will prompt you the first time you open this file. I recommend saying n to the prompt and tangling manually.

(add-to-list 'safe-local-eval-forms '(add-hook 'after-save-hook #'org-babel-tangle nil t))

Async

Tangling produces a noticeable pause every time I save. This should be pushed off the main thread.

We have to be careful about tangling with the org that was installed by straight, not the built-in one.

Faster Tangling

org-babel-tangle is really not that fast. It (absurdly, but perhaps unsurprisingly) opens the output file once for each tangled block. The mere act of loading org is also expensive in its own right. People have written custom functions that imitate the behavior of org-babel-tangle, but much faster. These are probably worth exploring.

Read/Edit/Compile Loop

Before I started using a tangled init, I could just edit init.el directly and eval-buffer to reload it. Having to org-babel-load-file and then select init.org is noticeably less convenient. I should make a wrapper for this. (And, while we’re at it, the wrapper should execute inside a single straight transaction, just like it would when init.el is actually loaded.)

I like straight philosophically - clones and lockfiles “feels” like the right way to solve this problem - and I don’t mind having to give up the built-in package management for those benefits. Extensive documentation is also greatly appreciated.

(setq load-prefer-newer t)
(setq straight-check-for-modifications '(find-when-checking))
(setq straight-use-package-by-default t)
(setq straight-cache-autoloads t)

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

(setq use-package-use-theme nil)

use-package is now part of emacs29, so I no longer have to install it. But for some reason, its custom theme behavior breaks some defcustoms in evil, so I disable it.

I disable straight’s automatic rebuilding and invoke straight-rebuild-package by hand. This avoids some nasty startup delays (#9/#41). I don’t need to rebuild all that often, so I don’t mind having to do it manually.

(use-package diminish
  :defer t)

Shallow Clones

straight has partial shallow clone support. However, freezing is not compatible with shallow clones, so we’re still waiting for that before we can adopt it.

org introduces special complications with shallow clones. The org-version function is not defined in the org-mode git repository; it’s supposed to be created by a Makefile. straight uses a special hack to fix that, but the hacks depend on the presence of tags in the cloned repository. Unfortunately, the current shallow clone implementation excludes tag refs, which causes org-version to return an error from git describe. (Tag objects will be fetched, but the refs to refer to them by name will not be created, and the refs are needed for git describe.) This creates another bad interaction with elfeed’s elfeed-link feature, which tries to invoke org-version when the org package is loaded. There would be a few ways to solve this problem:

  • Allow straight to preserve tags when shallowly cloning, maybe configurable with the existing :depth key.
  • Override the recipe for elfeed to exclude elfeed-link.el from the build, since I don’t use that functionality.
  • Do a deep clone of org instead of a shallow one. This is what I’m actually doing, since shallow clones are missing some functionality as I mentioned above.

One other interesting caveat (again, demonstrated by org) is that not all git servers support fetching specific commit refs. For example, with --depth=1, you can fetch 50fd89cb93 (the tag object for org release_9.4), but you can’t fetch fbccf09c74 (which is the actual commit the tag refers to). So using shallow clones would restrict you to released versions only. This has other complicated implications.

This keeps files like projectile-known-projects-file where they belong.

(use-package no-littering
  :demand t)

general provides a unified interface for binding keys. I use SPC and DEL as my leaders, since my keyboard puts them under my left and right thumbs.

(use-package general
  :demand t
  :config
  (general-override-mode 1)
  (general-create-definer private/with-leader
                          :prefix "SPC"
                          :non-normal-prefix "M-SPC"
                          :keymaps 'override
                          :states '(normal visual insert emacs))
  (general-create-definer private/with-local-leader
                          :prefix "DEL"
                          :non-normal-prefix "M-DEL"
                          :states '(normal visual insert emacs)))
(use-package hydra
  :defer t)
(use-package exec-path-from-shell
  :if (eq system-type 'darwin)
  :custom
  (exec-path-from-shell-check-startup-files nil)
  (exec-path-from-shell-variables '("PATH"
                                    "MANPATH"
                                    "GOPATH"))
  :config
  (exec-path-from-shell-initialize))

Thanks, Apple. This atrocious hack is dedicated to you.

UI Core

Built-ins

This is for built-in emacs miscellany that I want to reconfigure or turn off. There’s quite a bit of stuff in here.

(setq revert-without-query '(""))
(global-auto-revert-mode 1)
(setq auto-save-default nil)
(setq auto-save-list-file-prefix nil)
(setq create-lockfiles nil)
(setq make-backup-files nil)

(setq initial-major-mode #'org-mode)
(setq initial-scratch-message nil)
(setq inhibit-startup-screen t)

(setq sentence-end-double-space nil)

(tool-bar-mode 0)
(menu-bar-mode 0)
(blink-cursor-mode 0)

(setq ring-bell-function 'ignore)

(setq line-number-display-limit nil)
(column-number-mode 1)

(setq frame-title-format "%b")

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

(setq global-hl-line-sticky-flag t)
(global-hl-line-mode 1)
(show-paren-mode 1)

(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)

(setq uniquify-buffer-name-style 'forward)

(setq require-final-newline t)

(when (eq system-type 'darwin)
  (setq ns-command-modifier 'meta)
  (setq ns-option-modifier 'super))

Executable Script Hook

(advice-add #'executable-make-buffer-file-executable-if-script-p :before-while
            (lambda ()
              (and buffer-file-name
                   (not (string-prefix-p "." (file-name-nondirectory buffer-file-name))))))
(add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p)

This useful built-in function makes a file executable if it starts with a shebang. Unfortunately, this also hits my dotfiles (eg .bashrc), so I advise the function to skip any file with a leading dot.

Fonts and Faces

(cond ((eq system-type 'gnu/linux)
       (set-face-attribute 'default nil :family "Input"
                                        :height 100))
      ((eq system-type 'darwin)
       (set-face-attribute 'default nil :family "Menlo"
                                        :height 140)))
(set-face-attribute 'fixed-pitch nil :family 'unspecified
                                     :inherit 'default)

I have tried many techniques to configure emacs faces:

  • set-frame-font (or its deprecated cousin, set-default-font) are horribly broken if you use emacs in daemon mode. Because the initial emacs instance doesn’t have a GUI attached to it, something goes horribly wrong at init time and the fonts just don’t get set (1, 2, 3, 4). You end up with text that’s literally a couple of pixels tall. By the way, this is also true for terminal-local variables like window-system, which are not set at daemon initialization time.
  • default-frame-alist and window-system-default-frame-alist provide an alist with a font key, which lets you specify a string to use as the default font. However, emacs faces are quite a bit more complicated than that. On top of that, emacs’s fontconfig parsing seems to be highly nonstandard. Normally, the pattern Foo-10 (or equivalently Foo:size=10) specifies the height as 10pt, where as Foo:pixelsize=10 aims for a height of 10px. But in emacs, Foo:size=10 and Foo:pixelsize=10 do the same thing. I also find very different results between fc-pattern and describe-font using the same pattern (eg fc-pattern -d Input-10 pixelsize reports 10.4167px on my current monitor, but if I use Input-10 in emacs, describe-font shows the pattern Input:pixelsize=13).
  • face-spec-set lets you dig into the innards of an emacs face, but you have to specify the whole thing from start to finish. An emacs face actually has several layered attributes, and you probably don’t want to rewrite all of them just to change one or two.
  • custom-set-faces hooks into the Customize interface, which is the blessed high-level approach. However, Customize works by mutating your init file, which is not great if you’re an opinionated version control user.

After all of the above, I have settled on set-face-attribute for global faces. It lets me twiddle any individual part of any face (the full list of attributes is here) without going through Customize. For package-specific faces, use-package offers the :custom-face keyword, which goes through Customize while avoiding its major downside.

visual-line-mode

visual-line-mode is a built-in mode that truncates lines at word boundaries. adaptive-wrap-mode extends it to also preserve leading indentation.

(setq-default truncate-lines t)
(setq visual-line-fringe-indicators '(left-curly-arrow nil))

(use-package adaptive-wrap
  :hook
  (visual-line-mode . adaptive-wrap-prefix-mode)
  :diminish 'adaptive-wrap-prefix-mode)

I have not had positive experiences with this part of emacs:

I consider hard-filling paragraphs to be an ugly implementation detail that my editor is supposed to render irrelevant. It doesn’t help that auto-fill-mode is not applicable to everything I write. emacs is really not doing the job here.

generic-x

(use-package generic-x
  :straight nil
  :custom
  (generic-use-find-file-hook nil)
  :demand t)

Indentation

You can see that I set indent-tabs-mode to nil by default. I really do not like setting indentation behavior in my config. I used to use vim-sleuth and it was magical. You never had to tell it anything; it just knew what the right settings were. That’s what indentation configuration is supposed to feel like. I’ve heard that dtrt-indent can provide similar functionality for emacs. editorconfig support is also applicable to this problem.

I haven’t had to edit any “real” code in emacs yet, so remapping org-return-indent was sufficient for me, but I’d also like to look into electric-indent-mode (built-in) or aggressive-indent-mode to do this automatically.

I never really became fluent in vim, but my brief experience made it impossible to go back to any other editing system. The two big innovations of vim were:

  • separate modes for binding commands and inserting text
  • composable operators and text objects

I’m not married to anything specific in vim or evil besides those two principles, but nothing really comes close, and I’m not in the mood to roll my own version of evil right now.

(use-package evil
  :custom
  (evil-undo-system 'undo-redo)
  (evil-want-Y-yank-to-eol t)
  (evil-disable-insert-state-bindings t)
  (evil-motion-state-modes nil)
  :general
  (:keymaps 'override
   :states '(normal visual)
   ";" #'evil-ex
   "s" #'save-buffer
   "x" #'other-window
   "r" #'universal-argument)
  (:keymaps 'universal-argument-map
   "r" #'universal-argument-more)
  (private/with-leader
   "SPC" #'execute-extended-command
   ";" #'eval-expression
   "f" #'find-file
   "b" #'switch-buffer
   "h" #'help-command)
  (private/with-leader
   :infix "d"
   "" '(:wk "desktops"
        :ignore t)
   "d" #'evil-switch-to-windows-last-buffer
   "h" #'split-window-vertically
   "v" #'split-window-horizontally
   "x" #'delete-window
   "b" #'kill-this-buffer
   "k" #'kill-buffer-and-window)
  (:keymaps 'minibuffer-local-map
   "<escape>" #'minibuffer-keyboard-quit)
  :hook
  (private/evil-esc . (lambda ()
                        (when (minibuffer-window-active-p (minibuffer-window))
                          (abort-recursive-edit))))
  :demand t
  :config
  (advice-add #'evil-force-normal-state :after
              (lambda () (run-hooks 'private/evil-esc-hook)))
  (evil-mode 1))

I have a custom hook for when you press ESC in normal state, which I stole from doom. I tend to mash ESC when I want to get back to regular editor behavior, and this hook serves as a predictable entry point for that behavior.

Out of all the vim plugins in the world, surround is perhaps the only one that deserves to be built in. Naturally, there’s an evil version as well.

(use-package evil-surround
  :demand t
  :config
  (global-evil-surround-mode 1))

I have also been intrigued by embrace. It has an integration for surround, but if I was going to use it, I’d rather roll a brand-new evil wrapper that doesn’t depend on surround at all.

(use-package which-key
  :custom
  (which-key-echo-keystrokes 0.01)
  (which-key-idle-delay 0.5)
  (which-key-idle-secondary-delay 0.01)
  (which-key-popup-type 'minibuffer)
  (which-key-show-prefix 'top)
  (which-key-max-description-length nil)
  (which-key-compute-remaps t)
  (which-key-sort-order 'which-key-prefix-then-key-order-reverse)
  :demand t
  :config
  (which-key-mode 1)
  :diminish)

I could enable which-key-allow-evil-operators and which-key-show-operator-states, but choose not to because the popup is too large. There’s just too much information in there.

(use-package ws-butler
  :custom
  (ws-butler-keep-whitespace-before-point nil)
  :demand t
  :config
  (ws-butler-global-mode 1)
  :diminish)
(use-package ivy
  :custom
  (ivy-count-format "(%d/%d) ")
  :general
  ([remap switch-buffer] #'ivy-switch-buffer)
  (:keymaps 'ivy-minibuffer-map
   "<escape>" #'abort-recursive-edit)
  (private/with-local-leader
   :keymaps '(ivy-occur-mode-map ivy-occur-grep-mode-map)
   "DEL" #'ivy-occur-dispatch
   "RET" #'ivy-occur-press-and-switch
   "f" #'ivy-occur-press
   "a" #'ivy-occur-read-action
   "c" #'ivy-occur-toggle-calling
   "d" #'ivy-occur-delete-candidate
   "r" #'ivy-occur-revert-buffer)
  (private/with-local-leader
   :keymaps 'ivy-occur-grep-mode-map
   "w" #'ivy-wgrep-change-to-wgrep-mode)
  :demand t
  :config
  (ivy-mode 1)
  :diminish)
(use-package counsel
  :demand t
  :config
  (counsel-mode 1)
  :diminish)
(use-package ivy-hydra
  :commands (hydra-ivy/body))
(use-package swiper
  :general
  (private/with-leader
   "/" #'swiper))
(use-package wgrep
  :custom
  (wgrep-auto-save-buffer t)
  :general
  (:keymaps 'wgrep-mode-map
   [remap save-buffer] #'wgrep-finish-edit)
  :commands (wgrep-change-to-wgrep-mode))

Dismissing ivy-hydra

If I open ivy-hydra and then close the minibuffer, the hydra is actually still there. If I open the minibuffer, it becomes apparent that the hydra was open the whole time, and is eating all my keystrokes until I exit it with C-o. The hydra should terminate whenever the minibuffer closes.

Structured Find/Replace

This is a big topic, but I’m just going to stick it here because it’s all going through ivy one way or another.

swiper

swiper is my primary tool for structured find. It’s incremental (ie it shows me where I’m going before I decide to go there) and ephemeral (ie if I dismiss the minibuffer it leaves no traces of its presence). One useful addition would be an easy way to resume the previous swiper search. ivy-resume, maybe? I also don’t make much use of swiper-query-replace (M-q binding), which seems useful.

isearch

I have experimented with isearch (which is hooked into evil’s / by default). I find it most useful as a motion - ie when I already know exactly what I’m looking for with very high specificity - but avy works almost as well in those situations.

I don’t like using it for “searching”. Jumping around with nN is cumbersome, and often after a few jumps you realize that you should have refined the search expression a bit more. With swiper, you can just scroll the minibuffer, and if you need to narrow it down, you can type in more text. I’m considering just binding swiper directly to /.

occur/wgrep

I find wgrep very useful for transitioning from search to replace. The key sequences are not too difficult to remember: C-o to bring up hydra-ivy, u to occur, and DEL w to enable wgrep in that buffer.

rg

There’s probably some argument to be made for using rg (already projectile-integrated) in larger searches. We’ll see where that fits into the picture. I just haven’t used it enough yet. I believe the occur/wgrep system works just as well here as it does for swiper.

One thing I don’t like about counsel-projectile-rg is that it’s very difficult to constrain my search to a subfolder of the project. Perhaps deadgrep, which is highly rg-native, would be a good choice for a less incremental, more precise interface.

:s

For smaller find/replaces, I still use vim’s trusty :s (evil-ex-substitute). The syntax of :s lets you write the find and replace halves of the expression simultaneously in a very nimble way. Automatically reusing the last pattern from / is also a nice feature, although a bit niche. I only feel the need to do that when I’m replacing a fairly complex pattern, which is usually a sign to reach for another tool.

Once you start replacing a lot of stuff (more than a screenful) or really complicated stuff (anything involving eval-based expressions), :s becomes unpredictable and too cumbersome to use off hand. It works best when its effects are transparent and obvious.

Speaking of transparency, evil’s live preview for :s is extremely valuable. However, I’ve encountered some bugs with it (typically when replacing leading whitespace) where the preview markers don’t go away after the command is done.

It probably sounds like I like :s and I’m happy with its place in my workflow. For the most part, I am, but it’s literally the only ex command I use regularly. If I can replace it with something else, that lets me completely rebind ;: to other commands. visual-regexp or phi-search? My requirements:

  • robust live preview
  • edit find and replace sides simultaneously, ideally with similar syntax to :s
  • a quick keybind to jump from find to replace or vice versa (useful in longer expressions)
  • easy integration with swiper/rg and occur/wgrep, if you realize that you’re biting off more than you can chew

It’s also worth asking if we can scale :s to multiple files. A vim package that crossed my desk recently, and seems to have a very interesting workflow, is ferret. Something similar could probably be built on top of occur.

iedit/multiple-cursors

I’ve heard good things about iedit, and I’m also interested in multiple-cursors:

projectile with ivy integration

I mainly use projectile for fuzzy searching an entire project’s files and buffers. It’s quite refreshing to never think about which files are “open” and which ones aren’t. The concept of a “root” directory is also important for things like rg searching.

(use-package projectile
  :custom
  (projectile-ignored-project-function
   (lambda (project-root)
     (or (file-remote-p project-root)
         (string-prefix-p (straight--dir) project-root))))
  :demand t
  :config
  (put 'projectile-enable-caching 'safe-local-variable #'booleanp)
  (put 'projectile-indexing-method 'safe-local-variable
       (lambda (v) (member v '(native hybrid alien))))
  (projectile-mode 1))
(use-package counsel-projectile
  :general
  (private/with-leader
   :infix "p"
   "" '(:wk "projectile"
        :ignore t)
   "f" #'counsel-projectile-find-file
   "/" #'counsel-projectile-rg
   "p" #'counsel-projectile-switch-project
   "b" #'counsel-projectile-switch-to-buffer
   "k" #'projectile-kill-buffers)
  :demand t
  :config
  (counsel-projectile-modify-action
   'counsel-projectile-switch-project-action
   '((default counsel-projectile-switch-project-action-find-file)))
  (counsel-projectile-mode 1))

Demanding projectile causes its autoloaded functions to be bound under the C-c p prefix. However, if counsel-projectile hasn’t been loaded yet, the functions under that prefix will be un-counseled versions (because counsel-projectile-mode hasn’t run). I fix this problem by demanding both packages up front.

Finding Files vs Finding Buffers or Files

I used to use counsel-projectile, which lists buffers and files, but have now moved to counsel-projectile-find-file (with a wrapper when not in a project). This way, I can always navigate to a file by its project-rooted filename.

Consider a project with two files, foo/README and bar/README. If I open foo/README and then counsel-projectile, I will see README (the buffer for foo/README) and bar/README. This means there are no matches for foo/README. counsel-projectile-find-file avoids this problem.

Another issue arises if you have two separate projects, foo and bar, that each have their own README. If both ~README~s are open at the same time, the buffer names will be disambiguated by uniquify, which will appear in counsel-projectile. Again, counsel-projectile-find-file avoids this problem.

I also want counsel-projectile-switch-project to use counsel-projectile-find-file as its action (the default action selects a file or buffer, like counsel-projectile). The counsel-projectile-modify-action function lets us make this change in a reasonably ergonomic fashion.

Sorting

I mainly use buffer switching to cycle between the last few files I looked at. counsel-projectile supports sorting candidates, which might reduce my dependence on that functionality. Perhaps a binding for other-buffer would also help.

git-ls-files

projectile’s use of git-ls-files can lead to some strange behavior, because the list is based on the git index. This can lead to deleted files persisting, or duplicated listings for merge conflicts. I’m not actually sure there’s any way to get around this with a git-based command.

One of the unpleasant truths of vim is that, although there are structured motions for everything, you’re probably going to start out by holding down hjkl a lot. It takes a long time for all those other motions to seep into your muscle memory. avy provides a command that quickly gets anywhere on the screen, regardless of how the buffer is formatted. It reflects a “lazy vim” approach of using cheap, general commands that you’ll never have to think about.

evil actually defines motion wrappers for avy. However, its wrappers are inclusive, and I vastly prefer exclusivity for “jump to first instance” motions, so I redefine them.

(use-package avy
  :custom
  (avy-all-windows nil)
  :general
  (:states '(motion)
   "f" #'avy-goto-char-2)
  :config
  (evil-define-avy-motion avy-goto-char-2 exclusive))

Repeat

One nice feature of vim-sneak is that, after your initial search, you can mash the key to go to the next or previous instance. Such behavior could also be useful here. It would be something like this:

  • when you first press fF, you get prompted for the search argument (same as existing avy)
  • the matching candidates get highlighted under a trie (same as existing avy)
  • typing the keys for that candidate jumps you to it (same as existing avy)
  • after the first jump, mashing fF takes you to the next/previous instance of the same search argument
  • the jumplist only gets updated once for the entire search chain

Look into evil-snipe, perhaps?

Forget obtuse up/down/left/right-based window switching. It takes up a ton of binding space and it’s not even the fastest way to move around. ace-window lets you jump to any window with one key. You can hook into it to do a lot of other window-management-related things, but I use it for its barebones functionality, and it works like a charm.

(use-package ace-window
  :custom
  (aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
  (aw-scope 'frame)
  :custom-face
  (aw-leading-char-face ((t (:foreground "red"
                             :height 3.0))))
  :general
  ([remap other-window] #'ace-window)
  :init
  (setq aw-dispatch-alist '((?x aw-flip-window))))

Dispatch

You can do a lot of window-related stuff with aw-dispatch-alist, which could probably replace my entire SPC d leader tree. Definitely worth investigating. Integrating desktop management keybinds (eg eyebrowse, see below) would also be appropriate.

shackle keeps temporary windows out of the way. emacs has a nasty tendency to spawn them in the first free window it can find, and if you have your windows laid out just right, that’s usually not what you wanted. I’m used to vim’s “help pops up at the bottom” approach, and shackle lets me have that.

(use-package shackle
  :custom
  (shackle-inhibit-window-quit-on-same-windows t)
  (shackle-rules '((help-mode :select t
                              :popup t
                              :align below
                              :size 0.5)
                   (flycheck-error-list-mode :select t
                                             :popup t
                                             :align right
                                             :size 0.3)
                   (compilation-mode :select t
                                     :popup t
                                     :align right
                                     :size 0.5)
                   ("*Local Variables*" :select t
                                        :same t)))
  :demand t
  :general
  (:keymaps 'special-mode-map
   :states 'normal
   "q" #'quit-window)
  ([remap quit-window] #'private/quit-window)
  :config
  (defun private/quit-window (arg)
    (interactive "P")
    (quit-window (if arg nil 'kill)))
  (shackle-mode 1)
  :diminish)

*Local Variables* comes from hack-local-variables-confirm.

I remap quit-window so that it kills buffers by default instead of burying them. Since evil has its own binding of q in normal state, that has to be mapped back to quit-window.

Occur Buffers

ivy-occur buffers should be shackled to the window they were originally in. Jumping to candidates in the occur buffer should also be shackled (with the option of opening them in another window if explicitly requested, because sometimes that really is what I want).

Comprehensive Popup System

I rather envy doom-popups. This system hooks into evil’s normal state ESC to close the current window (if it is a popup), and to close all open popups (if it is not a popup). The definition of “popup” is applied through shackle.

This system has a few notable advantages. First, recycling ESC for this feels appropriate and avoids changing the normal state q binding. In addition, if I had an easy way to close popups without selecting them, I wouldn’t need as much :select t in my shackle rules.

(use-package flycheck
  :general
  (private/with-leader
   :infix "y"
   "" '(:wk "flycheck"
        :ignore t)
   "c" #'flycheck-buffer
   "C" #'flycheck-clear
   "v" #'flycheck-verify-setup
   "x" #'flycheck-disable-checker
   "RET" #'flycheck-explain-error-at-point
   "r" #'flycheck-display-error-at-point
   "y" #'flycheck-copy-errors-as-kill
   "j" #'flycheck-next-error
   "k" #'flycheck-previous-error
   "l" #'flycheck-list-errors)
  :hook
  (org-src-mode . (lambda () (flycheck-mode 0)))
  :demand t
  :config
  (put 'flycheck-ruby-executable 'safe-local-variable #'stringp)
  (put 'flycheck-ruby-rubocop-executable 'safe-local-variable #'stringp)
  (global-flycheck-mode 1))

Unfortunately, there’s no good way to run Flycheck across a tangled file when editing just one of the many blocks in that file. This leads to Flycheck getting very confused, so I turn it off in that context only. Note that you do need a hook for this, because flycheck-global-modes only checks major modes and org-src-mode is a minor mode.

Major Modes and Filetypes

(use-package org
  :custom
  (org-M-RET-may-split-line nil)
  (org-blank-before-new-entry '((heading . nil)
                                (plain-list-item . nil)))
  (org-startup-folded t)
  (org-catch-invisible-edits 'smart)
  (org-ellipsis "")
  (org-src-fontify-natively t)
  (org-src-tab-acts-natively t)
  (org-src-window-setup 'current-window)
  (org-file-apps '(("pdf" . system)
                   (auto-mode . emacs)
                   (system . "xdg-open %s")
                   (t . system)))
  :general
  (:states '(insert emacs)
   :keymaps 'org-mode-map
   "RET" #'private/org-return-indent)
  (private/with-local-leader
   :keymaps 'org-mode-map
   "h" '(private/hydra-worf/private/org-up-heading-safe
         :wk "parent heading")
   "j" '(private/hydra-worf/org-forward-heading-same-level
         :wk "next heading")
   "k" '(private/hydra-worf/org-backward-heading-same-level
         :wk "prev heading")
   "l" '(private/hydra-worf/private/org-goto-first-child
         :wk "child heading")
   "/" #'counsel-org-goto
   "r" #'org-reveal
   "e" #'org-edit-special
   "x" #'org-export-dispatch
   "RET" #'org-open-at-point
   "o" #'private/org-meta-return-after
   "O" #'private/org-meta-return-before)
  (private/with-local-leader
   :keymaps 'org-mode-map
   :infix "z"
   "" '(:wk "toggles"
        :ignore t)
   "h" #'org-toggle-heading
   "i" #'org-toggle-item
   "l" #'org-toggle-link-display)
  (private/with-local-leader
   :keymaps 'org-src-mode-map
   "e" #'org-edit-src-exit)
  :hook
  (org-src-mode . evil-normalize-keymaps)
  :config
  (defun private/org-return-indent ()
    (interactive)
    (org-return t))
  (defun private/org-meta-return-before (arg)
    (interactive "P")
    (beginning-of-line)
    (org-meta-return arg)
    (evil-append nil))
  (defun private/org-meta-return-after (arg)
    (interactive "P")
    (end-of-line)
    (org-meta-return arg)
    (evil-append nil))
  (defun private/org-up-heading-safe ()
    (interactive)
    (org-up-heading-safe))
  (defun private/org-goto-first-child ()
    (interactive)
    (org-goto-first-child)
    (org-reveal))
  (defhydra private/hydra-worf ()
    "navigate and move org headings"
    ("<tab>" org-cycle "cycle")
    ("h" private/org-up-heading-safe "parent")
    ("j" org-forward-heading-same-level "next")
    ("k" org-backward-heading-same-level "prev")
    ("l" private/org-goto-first-child "child"))
  (advice-add #'org-element-property :after-until
              (lambda (property element)
                (and (eq (org-element-type element) 'src-block)
                     (eq property :language)
                     "fundamental"))))
(use-package htmlize
  :defer t)
(use-package hydra-ox
  :straight hydra
  :general
  ([remap org-export-dispatch] #'hydra-ox/body))

Note that MELPA does not split hydra and hydra-ox into separate packages, so straight doesn’t know how to install hydra-ox. It has to explicitly be told that this package comes from the hydra repo. I would prefer to straight-get-recipe this, but hardcoding it is basically the same thing.

Navigation

I’m very fond of counsel-org-goto. It Just Works, which can’t be said for some of the things I tried in the past.

org has org-goto built-in. However, I despise org’s “open another buffer and fumble around in here” approach to navigation. You can customize org-goto to use ivy (org-goto-interface and org-outline-complete-in-steps), but I found that it choked on headlines with slashes in them. Perhaps it was an ivy bug.

Rather than investigate the slashes problem with org-goto, I tolerated counsel-imenu for a while. You need to futz around with some variables (imenu-auto-rescan, imenu-auto-rescan-timeout) to make it rescan every time you use it. The real problem is that it only displays leaf-level headings, so you can’t jump directly to intermediate headings.

I’ve also heard of some other options like deft, orgnav, and helm-org-rifle, but for now, counsel-org-goto is so close to my ideal implementation that I’m no longer shopping around. See also.

Out-of-Order Search

In my typical use of counsel-org-goto, I search for the last segment of the exact heading I’m aiming for. If that isn’t specific enough, I end up having to backspace over my search query and enter a higher-level heading first, to disambiguate. For example, in a file with headings foo/bar/baz and foo/qux/baz, I might search for baz, then have to backspace and search for bar baz.

The solution to this problem would be to relax matching order, so that baz bar could match foo/bar/baz. ivy--regex-ignore-order might be perfect for this.

Indentation

By default, plain text in org is indented to match the level of the headline. This is controlled by org-adapt-indentation, org-cycle-emulate-tab, and my binding of org-return-indent.

I actually like the indentation, because it helps distinguish headlines (you can scan the left edge of the buffer to locate them). It also increases the vertical density of my org files, since I don’t need empty lines (org-blank-before-new-entry) or larger fonts to make the headlines stand out.

org-src Default Language

I want to use fundamental-mode in org-src blocks that have no language, but there is no supported way to set a default language for org-src blocks. However, you can hack it in by advising org-element-property. If org-element-property returns nil for an org-src block’s language, this advice will treat the block’s language as fundamental instead.

evil-normalize-keymaps

Sometimes you’ll find your keymaps don’t work after changing modes or buffers, yet mysteriously pressing ESC gets them to behave. This is usually because you need to invoke evil-normalize-keymaps, hence my hook for org-src-mode.

A more powerful alternative to org-open-at-point. This should open the link at point (if any), and otherwise select one avy-style. Note that org-return-follows-link doesn’t work in evil normal state.

worf Tree Mutation

It’s fine to use counsel-org-goto for large jumps, but for shorter movements, it’s much faster to go up or down headings. worf has an especially elegant way of combining navigation and mutation of org trees. Unfortunately it doesn’t play nice with evil.

One important caveat of any up/down heading navigation is that it tends to pollute the jumplist. Ideally, you want to “enter” heading navigation mode, jump around headings freely, and add to the jumplist when you “exit” heading navigation mode. I used to have a hydra for this, and might rebuild it.

Some considerations for this development:

  • movements:
    • next heading:
      • any level:
        • org-next-visible-heading
        • outline-next-visible-heading
        • outline-next-heading
      • same level:
        • org-forward-heading-same-level
        • outline-forward-same-level
        • org-get-next-sibling
        • outline-get-next-sibling
        • org-goto-sibling
    • previous heading:
      • any level:
        • org-previous-visible-heading
        • outline-previous-visible-heading
        • outline-previous-heading
      • same level (note that, if we’re not on a heading, we want to back up to the current heading, not the one before it):
        • org-backward-heading-same-level: skips past current heading
        • outline-backward-same-level: same problem as org-backward-heading-same-level
        • org-get-last-sibling: doesn’t actually restrict point to same-level headings (it returns nil but the point still moves, which is almost definitely a bug)
        • outline-get-last-sibling: same problem as org-get-last-sibling
        • org-goto-sibling: same problem as org-backward-heading-same-level
    • parent:
      • org-up-heading-safe
      • org-up-heading-all
      • outline-up-heading
    • child:
      • org-goto-first-child
  • change:
    • item:
      ITEMorg-metaleftorg-metadownorg-metauporg-metaright
      headingorg-do-promoteorg-move-subtree-downorg-move-subtree-uporg-do-demote
      listorg-outdent-itemorg-move-item-downorg-move-item-uporg-indent-item
      tableorg-table-move-columnorg-table-move-roworg-table-move-roworg-table-move-column
    • tree:
      TREEorg-shiftmetaleftorg-shiftmetadownorg-shiftmetauporg-shiftmetaright
      headingorg-promote-subtreeorg-drag-line-forwardorg-drag-line-backwardorg-demote-subtree
      listorg-outdent-item-treeorg-drag-line-forwardorg-drag-line-backwardorg-indent-item-tree
      tableorg-table-delete-columnorg-table-insert-roworg-table-kill-roworg-table-insert-column
  • Can we use the ~:bind~ lambda to build bindings to the heads with general (lambda gets invoked here)? Or do we have to manually bind each head in private/with-local-leader?
  • We should have a toggle in the hydra to allow moving to invisible headings, which should default to off.
  • Should we also operate on lists? org-previous-item and org-next-item can navigate up/down, but they put the cursor in a stupid position. There doesn’t appear to be a way to navigate up/down levels of a list. In addition, org-next-item does nothing unless you’re already in a list. We may need to resort to parsing.
  • Similarly, support for tables would also be interesting, but there don’t appear to be good ways to jump “into” a table.
  • We should print a message to the minibuffer if we try to move past the end of a direction. ~save-excursion~ might help for this.
  • If existing org functions aren’t the right fit, maybe we can roll our own by parsing the file with org-element and om?

Target UX

  • heading state (default)
    hjkl (available outside hydra)
    parent heading, down same level, up same level, child heading
    v
    radio toggle between three states: always move to invisible, never move to invisible, only move to invisible if there is none visible (default)
    <tab>
    org-cycle
    c
    enter heading change state
    jk
    move subtree down, move subtree up
    hl
    promote subtree, demote subtree
    HL
    promote heading, demote heading
    q
    go back to heading state
    i (available outside hydra)
    enter list state
    hjkl
    superlist, down same level, up same level, sublist
    v
    radio toggle to enable moving to (and revealing) invisible items (default off)
    <tab>
    org-cycle
    q
    go back to heading state
    c
    enter list change state
    jk
    move item tree down, move item tree up
    hl
    outdent item tree, indent item tree
    HL
    outdent item, indent item
    q
    go back to list state
    t (available outside hydra)
    enter table state
    hjkl
    left cell, down cell, up cell, right cell
    q
    go back to heading state
    c
    enter table change state
    jk
    move row down, move row up
    hl
    move column left, move column right
    JK
    insert row, delete row
    HL
    delete column, insert column
    q
    go back to table state

Completion

I hate typing out org keywords (#+BEGIN_SRC, etc) by hand. You can type them in lowercase (which I should really start doing), but even better would be autocomplete for them. Autocompletion is unfortunately a TODO in its own right, but perhaps we can hack up an interim solution with ivy.

While I prefer working in org, sometimes you have to write markup that other people can edit, and org is really not usable in any editor but emacs. In those situations, Markdown is basically inevitable.

(use-package markdown-mode
  :custom
  (markdown-hide-urls t)
  :mode "\\.md\\'"
  :hook
  (markdown-mode . visual-line-mode))
(use-package edit-indirect
  :defer t)

The earliest incarnation of beancount-mode was a minor mode, so that it could be embedded in an org-mode file. The modern version is a major mode, but my beancount file still uses org-shaped stuff, so I use outshine to preserve the behavior I used to depend on. (beancount-mode includes some org-esque cycle functions, but I want other outshine functionality as well, like org-style behavior at the beginning of the buffer. So I use outshine’s implementation instead of beancount’s.) I might break the outshine bits into their own config if I ever use it in non-beancount contexts.

Not having full org-mode powers is a genuine downside. For example, counsel-outline has the same functionality as counsel-org-goto, but in an org buffer, it invokes org-goto-marker-or-bmk, which reveals the heading you’re jumping to if it’s hidden underneath another heading. That reveal doesn’t happen in outline or outshine, so you end up selecting the hidden text instead, and pressing TAB expands the visible heading that you’re on instead of the actual heading you jumped to.

I don’t use beancount alignment at all, so I advise away that part of indent-line-function.

(use-package beancount
  :straight (:host github
             :repo "beancount/beancount-mode"
             :branch "main")
  :custom
  (beancount-use-ido nil)
  :general
  (:states '(normal insert emacs)
   :keymaps 'beancount-mode-map
   "C-c d" #'beancount-insert-date)
  (private/with-local-leader
   :keymaps 'beancount-mode-map
   "b" #'private/beancount-balance-sheet
   "q" #'beancount-query
   "l" #'beancount-check
   "x" #'beancount-context)
  :mode ("\\.beancount\\'" . beancount-mode)
  :hook
  (beancount-mode . (lambda ()
                      (outshine-mode)
                      (goto-char (point-max))
                      (outline-show-entry)))
  :config
  (defun private/beancount-balance-sheet ()
    (interactive)
    (let ((compilation-read-command nil))
      (beancount--run beancount-query-program
                      (file-relative-name buffer-file-name)
                      "select account, sum(units(position)) as position from clear where account ~ 'Assets'or account ~ 'Liabilities'group by account, currency order by account, currency")))
  (advice-add #'beancount-align-number :override
              (lambda (&rest r) ())))
(use-package outshine
  :custom
  (outshine-startup-folded-p t)
  (outshine-cycle-emulate-tab t)
  (outshine-org-style-global-cycling-at-bob-p t)
  :general
  (:states '(normal insert emacs)
   :keymaps 'outshine-mode-map
   "TAB" #'outshine-cycle)
  (private/with-local-leader
   :keymaps 'outshine-mode-map
   "/" #'counsel-outline)
  :diminish 'outline-minor-mode
  :defer t)

Mode Improvements

beancount-mode is rather anemic, and there’s a lot of stuff I would like to improve:

  • fontification of comments, strings, numbers, and commodities
  • beancount-account-regexp does not recognize custom naming options (see beancount-account-categories)
  • autocompletion for accounts and payees
  • clean auto align for the entire file, even for non-transaction directives (bean-format can help, but it only aligns amounts)
  • Flycheck invocation of bean-check
(use-package systemd
  :defer t)
(use-package yaml-mode
  :defer t)

The docs for this mode mention that you have to bind RET yourself if you want auto-indenting, but evil seems to have me covered there.

Frankly, this mode is not very good, but that’s not its fault. It’s just that YAML is incredibly difficult to parse correctly. This leads to some delightful bugs which are probably never going to be fixed.

(use-package go-mode
  :custom
  (gofmt-show-errors nil)
  :hook
  (go-mode . (lambda () (add-hook 'before-save-hook #'gofmt-before-save nil t)))
  :defer t)

We don’t want to add gofmt-before-save to the global before-save-hook, because that would cause go-mode to be loaded in every buffer, whether it was a go buffer or not. Instead we add to the local before-save-hook. We then have to explicitly request deferred loading. Normally :hook implies :defer t, but only if the target of the hook is a function symbol. If it’s a lambda, then use-package will resort to its default behavior of demanding the package, to ensure that the package is loaded when the lambda runs. In our case, we know the lambda doesn’t need that, so we can safely ask for deferral.

(use-package go-eldoc
  :hook
  (go-mode . go-eldoc-setup))

See also: company-go.

(use-package rust-mode
  :custom
  (rust-format-on-save t)
  :defer t)
(use-package flycheck-rust
  :hook
  (rust-mode . flycheck-rust-setup))

See also: racer.

Ruby

(setq ruby-insert-encoding-magic-comment nil)

See also: enhanced-ruby-mode and robe.

(use-package elfeed
  :general
  (:keymaps 'elfeed-search-mode-map
   :states 'normal
   "q" (lambda ()
         (interactive)
         (elfeed-db-save)
         (kill-this-buffer)))
  (private/with-local-leader
   :keymaps 'elfeed-search-mode-map
   "g" #'elfeed-search-update--force
   "G" #'elfeed-search-fetch
   "RET" #'elfeed-search-browse-url
   "y" #'elfeed-search-yank
   "s" #'elfeed-search-live-filter
   "S" #'elfeed-search-set-filter
   "u" #'elfeed-search-tag-all-unread
   "r" #'elfeed-search-untag-all-unread)
  :defer t
  :config
  (let ((opml (no-littering-expand-var-file-name "elfeed/elfeed.opml")))
    (when (file-exists-p opml)
      (elfeed-load-opml opml))))

I actually don’t read feed items in emacs at all. I vastly prefer the rendering of my browser and would prefer to handle all my feeds there. Unfortunately, my old feed reader (Sage++) died in the Firefox 57 WebExtensions migration, and I have yet to find anything remotely satisfactory to replace it. While I plan to write my own feed reader someday, elfeed is a pretty reasonable feed organizer, and it lets me do the reading in the browser, so it’ll do for now.

I don’t want to store my feeds list in git, so I currently load it from an OPML file rather than using elfeed-feeds. There is probably a good way to store elfeed-feeds in a separate file (similar to projectile-known-projects-file) but I haven’t bothered to implement it yet.

elfeed-db-compact

I didn’t even know elfeed-db-compact existed until very recently. It greatly reduces the number of stray inodes running around in my no-littering var directory. I was going to run it on a hook whenever I exited elfeed, but it seems to be quite slow. If I hook it to kill-emacs-hook, it might not get run depending on how emacs terminates. I’ll have to figure out some kind of automation here.

Other Improvements

More File Types

Spacemacs layers for various languages can give us useful direction on this subject.

LSP

The Language Server Protocol gives me hope that my editor will stop being completely terrible some day. A list of implementations can be found here. More thoughts here.

tree-sitter

Here.

epub

See nov.el.

Bash

See company-shell.

Python

See elpy, anaconda-mode, company-anaconda, and yapfify. (elpy vs anaconda: further reading.)

Preserving Locals After Major Mode Change

An excellent write up on this topic is here. Opening a file runs normal-mode, which invokes hack-local-variables to set dir and file locals. But when a new major mode is run, the call chain propagates up to its parent, fundamental-mode, which runs kill-all-local-variables. hack-local-variables doesn’t get called again, so the local variables are lost.

You can add hack-local-variables to after-change-major-mode-hook to ensure that it gets rerun after any major mode change. However, normal-mode also runs set-auto-mode, which performs major mode autodetection and also triggers that hook. So if you add hack-local-variables to that hook, then normal-mode will run it twice. It’s unclear if this is actually harmful, but it’s probably wrong.

The solution in that Stack Overflow answer is to add hack-local-variables to the hook, but with a flag to skip it. Then you advise normal-mode to set the flag, so when set-auto-mode triggers the hook, hack-local-variables gets skipped. normal-mode will then invoke hack-local-variables directly to achieve the original effect. Meanwhile, other major mode changes will run the hook with the flag unset, so hack-local-variables will be rerun as desired.

I like the concept of this solution, but it also feels ugly. Maybe there’s a way to add some :before-while advice to hack-local-variables, to achieve the same thing without a custom flag. Needs more investigation.

Note that, if a file’s major mode is configured by a local variable, rerunning hack-local-variables makes it impossible to change that major mode manually. If you attempted to do so, hack-local-variables would detect the local variable and immediately change the mode back. Maybe we could add a flag to hack-local-variables to skip major modes. (It currently has a flag that does the opposite - major modes only.)

Modeline and Frame Title

I’m pretty happy with the built-in emacs modeline in terms of information, but it doesn’t look flattering. Could use some customization. Matching improvements for frame title would also be appropriate.

Pairs

Automatic pair insertion saves a lot of time and generally reduces the cognitive load of keeping parentheses matched. As emacs is a lisp-heavy environment, a number of specialized packages exist specifically for lisp’s uniquely paren-intensive requirements. An interesting overview was written here. Much ink has been shed on this topic, such as here.

While we’re on the subject of lisp, it would be nice to fix indentation of keyword blocks, as described here. One example of this in my config is in the :general sections of my use-package forms.

Outside of lisp, it’s still useful to have automatic pairs, but you don’t really need anything else. Besides smartparens, there’s also the built-in electric-pair-mode.

It would also be nice if evil’s % motion worked with arbitrary pairs, like in vim. That functionality can be achieved with evil-matchit.

Comments

emacs has two built-in commenting functions, comment-dwim and comment-line. There are some packages as well:

Autocompletion

Autocompletion is a huge time saver and can eliminate a lot of “whoops I forgot that argument’s type” brain cycles. Unfortunately, the situation in emacs is not great. There are two main implentations, company and auto-complete. Some other interesting thoughts here.

git

Obviously the elephant in this room is magit, with support from other packages like magithub and evil-magit. Some other important considerations:

I also want good gist support, which I believe is built into magit, but there are also some interesting third-party alternatives, like webpaste.

Desktops

My goal is to have window arrangements segregated by project, like persp-projectile. However, you need to have desktop management first to implement that, so I’m looking at using eyebrowse with some hand-rolled projectile integration. It’s also worth exploring wconf, or the built-in winner-mode. Also: zoom, purpose.

Scroll

I’m pretty comfortable with emacs’s default scrolling behavior, but here are some packages to investigate:

Dired

I use ranger as my file manager these days. Theoretically, there’s no reason I couldn’t do that in emacs instead. However, vanilla dired is not fun. It’s a pain to teach dired to open things in their native programs rather than in emacs. So there’s a lot of work that needs to be added here:

File Tree

In practice, I vastly prefer navigating projects with recursive fuzzy search, as already provided by counsel-projectile. But there’s something to be said for an interactive file tree when exploring a project whose structure you don’t yet know. emacs has a number of options here:

mpd

I grudgingly use ncmpcpp as my mpd client right now, but its interface is not customizable enough for my tastes. I would like a tree by genre/album/track/artist in that order (cmus has a tree, but it’s artist/album only with no other options). What better place to implement a highly customizable text-based UI than emacs?

Miscellaneous Packages