YouTube Summaries

← All summaries

Per Nordlöw's Emacs extensions and non-modal tooling

2026-08-19 Wed ⏱ 1 hr 50 min protesilaos

Per Nordlöw - Swedish developer, ~30 years in the industry, recently consulting in finance - screen-shares a handful of his custom Emacs extensions. His organizing principle is that interfaces should be non-modal, and his most interesting work is a "super-linter" that turns compile time, unit-test failures and coverage gaps into live Flymake diagnostics. His stated goal for the talk is to get others to pick up and upstream this work, because he has no time to.

Non-modal as a design principle

He cites Jef Raskin's The Humane Interface. The aviation argument: cockpit controls return to their original position when released, so pilots under stress never have to recall which mode they are in. He applies the same test to editing.

That is his objection to yasnippet: while filling in a snippet's fields, buffer interaction is fundamentally different - which breaks the common case of expanding a snippet inside a field of another snippet.

Prot notes Magit deviates from this, and they agree it is closer to completing a key sequence or acronym-expanding a git subcommand than a true mode.

tempo.el

Instead he uses tempo.el (built into Emacs, written by David Kågedal at Linköping, where Per studied), with an ~800-line modified version of his own plus a tempo-collection of per-language definitions. Why he prefers it:

  • Templates are written in Emacs Lisp rather than a separate DSL (the yasnippet/TextMate format's portability being the counter-argument), and macro expansion makes debugging easier.
  • Field navigation is non-modal: M-n moves to the next field, and the field positions of previously expanded snippets remain reachable, so code keeps behaving like a form. Persistence is partial - it holds across a couple of expansions, not indefinitely. He and Prot discuss anchoring the positions to tree-sitter nodes, or using text properties, to make it durable.
  • Aliases: any number of abbreviations per template, useful when different people (or the same person on different days) reach for different names.
  • Keyword auto-expansion: because if can only ever mean an if statement, pressing space expands it, controlled by the first parameter of each template's spec.
  • tempo-expand-if-complete-or-read (on C-M-RET): expands a unique abbreviation, otherwise offers completion over all templates - useful when you want if vs if-else, or only remember the first letter.

Prot suggests adding a completion annotation function so candidates show the template body inline.

Flymake extensions

He moved from Flycheck to Flymake because Flymake is built in, is what Eglot uses, and is more flexible about spawning subprocesses and collecting output (at the cost of being more verbose to write).

  • Timing/memory in the mode line :: shows the milliseconds and megabytes the checker consumed. Critical for statically typed languages with compile-time evaluation: adding import std jumps it to 250 ms, and a runaway template recursion is immediately visible. He uses it to keep compile times down and to compare alternative implementations, and says he has not seen it anywhere else. It is language-agnostic.
  • End-of-line diagnostics :: copied from Neovim's inline presentation, which he found better for bandwidth to the reader; partly vibe-coded, overlaid via hooks/advice. Prot points out Emacs 30 introduced built-in Flymake end-of-line diagnostics, improved in 31 with further refinements in 32, including several display styles and options for showing only the current line - Per is surprised, and jokes that someone may have picked up his code.

xchk - a super-linter

A ~600-700 line D application that spawns several subprocesses and merges their output into Flymake, tagged by subtask (chk, run, cov, and planned lint/benchmark/profile).

The design insight: run the cheapest analysis in parallel with the expensive ones and report asynchronously, so syntax errors surface before anything that depends on them finishes. A lex/parse/semantic-only check (one DMD flag, no codegen, no test execution) is far faster than a full compile - up to 10x for LLVM-backed languages like C3, where the highly optimized front end is only 5-10% of compile time because LLVM prioritizes generated-code quality over compile speed.

What it surfaces, demonstrated live:

  1. chk - semantic errors, merging multiple errors on one line.
  2. run - it detects a unit test in the file, recompiles with -unittest -main -run, and reports the failing assertion with its expected value as a diagnostic. Per notes the left-hand subexpression could be annotated more clearly.
  3. cov - coverage analysis listing uncovered lines, so an uncommented new function immediately shows as uncovered. He asks rhetorically how many IDEs do this generically.

With all three at zero you know simultaneously: no semantic error, tests pass, and the file is fully covered. He calls this a productivity game changer for building stable software after years of use.

This is also his argument for placing unit tests immediately after each definition in the same file: it keeps test and definition in sync, and it lets the build recompile the minimum, since a separate test file would require cross-file dependency and symbol-resolution analysis. He and Prot note Emacs Lisp would need the same kind of xref/symbol resolution.

Performance: a 2,000-line file importing tens of thousands of lines takes 0.4 s and 260 MB in the parent process (peak memory is tracked too); 0.5-2 s for really large work. It is D-specific - flags and compiler phase structure differ per language - with no abstraction layer yet, though he is open to helping anyone who wants one. It depends on his own nxt namespace of D extensions, written because the standard library's template expansions are poorly optimized for compile time. He also uses type-driven development with paths to catch logical errors at compile time.

Wishlist: benchmark and profiling subtasks collected incrementally (they would take tens of seconds to minutes), and per-subtask status indicators in the mode line rather than Flymake's single indicator. Prot notes the mode-line part is doable independently since the data is already collected, and that filtering the diagnostics buffer by backend would really be a generic tabulated-list-mode filter.

Character-edit on an active region

Instead of self-insert replacing the selection, a keypress on an active region performs an operation on it. In Org, c wraps as code, i italic, b bold; in code, s single-quotes, d double-quotes, q back-quotes, raw string literals per language. The motivation is keyboard ergonomics - ASCII punctuation to the right of the pinky is awkward even after he moved from the Swedish to the UK layout. It is context-sensitive: inside a D comment, c produces D's code-quoting construct instead. Discoverability is the open problem; they discuss a compact one-line hint popup, ideally on demand rather than eagerly, to avoid the minibuffer resizing.

File and project commands

  • Rename buffer and maybe file :: bound over C-x C-r (find-file-read-only, which neither of them uses). Renames the buffer, the file if there is one, and handles VC unregister/register. Demonstrated offering to delete the corresponding .elc, which is a separate hook - an example of Emacs letting you separate concerns via hooks and advice, with the corresponding responsibility to keep the scope narrow. He argues byte-compiled files should not live next to sources at all. He also has a kill-buffer-and-delete-file command; its VC behaviour he is unsure about and skips.
  • project-replace-string / regexp :: an unattended project-wide replace with no per-occurrence prompting, symbol scoping, file-pattern filtering, and history remembering the from/to pair like query-replace. Deliberately pure Emacs, no external tool, because every external tool's regexp dialect and backslash escaping differs. He notes LSP already has symbol rename and he has considered dispatching to it, but LSP does not cover references in documentation. Asked whether the file pattern should default to the current file's extension, he objects - that would be a modal default.
  • project-perform-action (on F10) :: collects the actions available for a project on the fly and offers them for completion, merging VC operations with build-tool targets (dub as the D default, make, and so on). Extensibility uses the same CL object-oriented dispatch Emacs already uses for VC: keyed on a =(vc Git . directory)=-shaped structure, returning a list of conses of subcommand name and Emacs Lisp function, so a (build make) key yields a different action set. Neovim and the Nix community are pursuing similar ideas.

The payoff case: building the D compiler has enough quirks - selecting a specific test in the suite, for instance - that he wrote a build debug action which compiles and then, from the process sentinel, launches GDB (realgud when available) on the freshly built compiler binary, reloading it if the debugger is already open. Live demo fails because upstream just removed a codegen regeneration make target - "what happens when you showcase repos in flux". Notably little code for what it does.

His closing observation: workflows this cumbersome stay unfixed for decades because nobody measures the payoff of fixing them, and in a community-driven project the incentive is weaker still. There is enough left over for a follow-up conversation.