cpg

Doom Emacs tips

2024-08-18 #emacs

Useful configuration, commands, bindings and workflows

Contents

After many years of manually assembling an Emacs configuration from scratch, I eventually switched to Doom Emacs a couple of years ago, discovering in the process that Emacs could do a lot more things than I thought, in a more user-friendly way.

This is...not an inaccurate portrayal.

Default Doom keybindings are organized in logical groups (SPC s for search, SPC b for buffers, SPC w for windows, SPC p for projects, SPC f for files…), and pausing after a partial combination will show an overlay with available options. However, the sheer amount of commands and bindings makes discovering the most useful ones somewhat difficult for newcomers.

Keybindings starting with SPC, as shown by pressing SPC and pausing.

Accordingly, this page contains hints about useful commands, bindings and configuration snippets for Doom, targeted towards “the colleague I just convinced to give Emacs a try”.

Of course, see also the official Doom documentation, which you can read directly in Emacs with SPC h d h.

Note: Parts of the following assumes that you have enabled the required features in your init.el and ran doom sync. Compared to the default configuration, you likely want to enable :tools lsp, (format +onsave), and the :language that you use. Configuration changes go to config.el.

Opening files (SPC f)

Searching inside files (SPC s)

Switching between buffers (SPC b)

Windows

Whole buffer

Use / to search, n/N to navigate the matches forwards/backwards after pressing ENTER.

As a variant to /, SPC s s shows all the matches in an overlay, and highlights them in the main buffer .

Avy allows quickly jumping to a match from a keyboard-based decision tree (centered around your home row).

Line

In addition to the usual 1-character motions (find f/F and to t/T), evil-snipe provides line motions with 2-character search: press s (or S for backwards) and type the first two characters. Navigate multiple matches with s/S again. In operator mode, this becomes z/Z and x/X.

Typing s i from the start of the line. Typing t will then jump to the middle it.

This only works for the current line. See the previous section for navigating across lines.

Editing

evil-mode editing tricks

By default, Doom emacs uses vim bindings with evil-mode. Get familiar with keys such as y, d, g, :, the modes (command, insert, visual, visual block), etc. See also the documentation.

Flycheck

Flycheck provides syntax checking (with a very generic meaning: from code to natural language), supporting plethora of backends.

Formatters

Doom uses apheleia to efficiently format-on-save code from a wide variety of languages.

Formatters can be added or modified easily:

(after! format
  ; Edit an existing formatter
  (set-formatter! 'black '("blackd-client" "--line-length" "119"))
  ; Add a new formatter
  (set-formatter! 'typstfmt '("typstyle" "-c=100"))
  (setq-hook! 'typst-ts-mode-hook +format-with 'typstfmt)
)

Snippets

Yasnippet is a snippet templating engine, which help avoid repeatedly typing boilerplate. In any mode, call M-x yas-describe-tables to see the available snippets; Doom ships with a relatively large library.

Yasnippet tables in rust-mode

Snippets are usually activated with a prefix and TAB. For many snippets, pressing TAB after the snippet activates also allows jumping between the different fields. For example, the function snippet in rust-mode (activated with fn) is defined as:

fn ${1:function_name}($2) ${3:-> ${4:i32} }{
   $0
}

Upon typing fn TAB, the user will be allowed to enter the function name, arguments, optional return type, and finally body.

New snippets can be created with M-x yas/new-snippet.

Enable yasnippet in all modes with

(yas-global-mode)

in your config.el.

Magit

A major selling point of Emacs in general is the presence of Magit, the text user interface for Git.

Main screen

The main screen is opened with SPC g g. The ? key lists available comments.

Some of the most useful commands are:

Hunks can be staged or unstaged with s and u, which also works on sub-hunks by selecting regions and using the same keys. This is an extremely powerful feature to split complex changes into smaller commits.

The stashes can be opened with ENTER. They can be applied totally with Z p, or selectively with a on hunks or sub-hunks or interest after opening the stash.

From a commit, it is possible to open the old version of a file by pressing ENTER on one of the old (red) hunks.

Use x to untrack a file or remove an untracked file (magit never deletes files, it puts them in the trash).

Buffers

The :ui vc-gutter displays the version control status of sections.

Extensions

Language servers

Language servers have become the standard for providing language support across code editors. The lsp-mode supports all current features and many servers/languages. In many cases, it is able to install the server by itself, if desired.

The most useful commands are:

The following options are useful. They set a response timeout, enable inlay hints, but disable inline documentations, which may be found intrusive and not extremely helpful.

(setq lsp-inlay-hints-mode t)
(setq lsp-inlay-hint-enable t)
(setq lsp-ui-doc-enable nil)
(setq lsp-signature-render-documentation nil)
(setq lsp-response-timeout 30)
(setq lsp-modeline-code-action-fallback-icon "󰌵")

When something goes wrong, SPC b B will allow accessing the lsp-err message with the stderr of the server.

lsp-booster

Even though Emacs now has native JSON parsing, lsp-booster can speed up processing and avoid UI locks, by offloading decoding to a separate Rust binary. To install it with doom:

Rust and rust-analyzer

The de facto standard language server for Rust is rust analyzer. My configuration reads as follows:

(setq lsp-rust-analyzer-cargo-override-command ["/home/cpg/check.nu"])
(setq rustic-cargo-check-arguments "--workspace")
(setq lsp-rust-analyzer-diagnostics-disabled ["unresolved-proc-macro" "unresolved-macro-call"])
; Types can get really long (e.g. with axum)
(setq lsp-rust-analyzer-max-inlay-hint-length 20)
(setq lsp-rust-analyzer-server-display-inlay-hints t)
(setq lsp-rust-analyzer-proc-macro-enable nil)
(setq lsp-rust-analyzer-experimental-proc-attr-macros nil)
(setq lsp-rust-analyzer-cargo-run-build-scripts nil)
(setq lsp-rust-all-features t)
(setq lsp-rust-analyzer-cargo-all-targets nil)

The first line is a hack to avoid the entire workspace being cargo check’ed whenever a workspace member changes. The nushell script simply wraps cargo check and passes the package of interest from a YAM list. This does not seem to be possible with the current options; an alternative is to open only one package in the LSP.

# - package1
- package2
#!/usr/bin/env nu
def main [] {
    mut package = "--workspace"
    $package = (cat ~/ra-check-mode.yaml | from yaml | first | str trim);
    $package = $"-p=($package)"
    cargo check --tests --all-features --message-format=json $package
}

Org mode

Next to Magit, Org mode can certainly be considered another killer feature of Emacs. Text-based productivity tools like Obsidian or Logseq have actually seen a rise in popularity.

According to the homepage, Org mode is

[a] major mode for keeping notes, authoring documents, computational notebooks, literate programming, maintaining to-do lists, planning projects, and more — in a fast and effective plain text system.

Starting with Org mode is very simple: write your note in plain text and let the mode take care of the formatting:

See https://orgmode.org/features.html for a more complete overview of features, and then the manual (or its compact version) or the work knowledge base.

Dired (file navigation)

Dired is a file explorer integrated in emacs. Open it by pressing ENTER on the first line of SPC f f, after navigating to the folder of your choice. Alternatively, use C-x d.

A dired buffer. Sorry, my fr_CH time locale shows up.

Some useful commands:

Given that the listings are regular Emacs buffers (after all, everything is a buffer), the usual search/navigation commands.

Similarly to the edition of search results, one can actually turn a Dired buffer into write mode to batch rename files, removing the need for tools like rename:

  1. C-c C-e.
  2. Edit files.
  3. C-x C-s to save.

Miscellaneous

Things I am not using (yet)