Massimiliano Mirra

Notes



















Date:
Tags: emacs
Status: finished

Lightweight external command integration in Emacs via compilation mode

For many tasks in a developer’s day, a well-tuned compilation mode is more efficient than reaching for a shell (fewer keystrokes, less context switching) and lighter on the brain than specialized major modes (no new key bindings).

Emacs’s “compilation” mode is not limited to running compilers. You can run bundlers for front-end development, test runners for red-green-refactor TDD, project-wide linters, dev servers for back-end code, Makefile targets, and more.

These notes cover:

  • handling commands that produce rich output, and/or clear the screen between multiple runs in a single long-lived session
  • choosing efficient key bindings
  • browsing command output without leaving the source window
  • selecting commands from configurable, contextual lists using the run-command package

Rich output and long-lived commands

Colors are often the only way to notice errors in a flurry of output. Also, commands with a long startup time sometimes offer a “watch” mode to do several runs for a single invocation, and clear the screen between runs.

Compilation mode by default handles neither the control characters that produce colors, nor those that clear the screen. A simple solution that covers many cases is to watch out for a few sequences in the command output using a “compilation filter”, and apply them:

(defun local/postprocess-compilation-buffer ()
  (goto-char compilation-filter-start)
  (when (looking-at "\033c")
    (delete-region (point-min) (match-end 0)))
  (ansi-color-apply-on-region (point) (point-max))))

(add-hook 'compilation-filter-hook 'local/postprocess-compilation-buffer)

Some commands produce even richer output (progress bars, simple animations) or clear the screen by other means (ANSI codes), requiring more complete terminal capabilities. The run-command package (described later) can run such commands in Emacs’s term-mode, providing those capabilities, while maintaining compilation-mode functionality.

Choosing efficient key bindings

The basic way to run a command in compilation mode is M-x compile RET <command> RET, e.g. M-x compile RET npm start RET. Next time you invoke compile, <command> defaults to the previous command, so recompiling effectively becomes M-x compile RET RET.

However, recompiling being the more common case, Emacs also provides M-x recompile, which reuses the last command right away, without prompting. (You can still edit the command with C-u M-x recompile.) So, recompile is the better candidate for quick access via a key binding, e.g.:

(global-set-key (kbd "C-c c") 'recompile)

With that, C-c c will re-run the last command, and C-u C-c c will let you edit the command first.

There’s an even shorter binding for when the cursor is already in the compilation window: g. This is useful when you need to alternate among several complex commands: leave their buffers open, then switch back to them and press g as needed.

To stop a command, move the cursor to the compilation buffer and press C-c k.

Browsing command output without leaving the source window

Unless you need to copy a command’s output, or re-run a previous command, there’s little reason to move the cursor to the command output window; you can more efficiently browse it without leaving the window where you’re writing using these key bindings:

  • To scroll to the beginning of the output: M-home
  • To scroll to the end of the output: M-end
  • To scroll the output forward: M-next (where “next” means PageDown) or C-M-v
  • To scroll the output backward: M-prior (where “prior” means PageUp) or C-M-S-v

These bindings are not specific to compilation mode, the work in every situation where there is an “other window”. They may seem cumbersome, especially on laptop keyboards which require an additional Fn press, but are well worth the effort.

Another flow saver, provided that the command prints errors in a format that compilation mode understands, is C-x `, which jumps to errors in the output, in succession. For example, given a TypeScript project with a file src/App.tsx:

import React from 'react'
import ReactDOM from 'react-dom'

console.helloWorld()

Running M-x compile RET npxj tsc -P . --noEmit --watch will launch the type checker and print the following:

[8:13:36 PM] File change detected. Starting incremental compilation...

src/App.tsx:3:9 - error TS2339: Property 'helloWorld' does not exist on type 'Console'.

3 console.helloWorld()
          ~~~~~~~~~~

[8:13:36 PM] Found 1 error. Watching for file changes.

If you now hit C-x `, the App.tsx buffer will appear, with the cursor on helloWorld.

Selecting commands from configurable, contextual lists using the run-command package

If, in a given context, you tend to run the same small subset of commands over and over, (e.g. npm test:watch in a JavaScript project, make clean in a C project, hugo server --buildDrafts in a Hugo blog), that’s a prime case for automation.

The run-command Emacs package (by the author) lets you write simple command lists, bring them up depending on the context, and select them with autocompletion:

run-command demo

See the run-command GitHub page for setup and usage instructions.

Additional customization

  • To have the window scroll as output comes in, set compilation-scroll-output to t.
    • The default scrolling mode often leads to a large amount of empty space at the bottom of the window; this is due to “recentering”. To prevent that and display as much output as possible, set scroll-conservatively to 101 (see M-x describe-variable scroll-conservatively RET for why).
  • To have it scroll, but stop scrolling when the first error is detected, set compilation-scroll-output to error.

I write in the hope of making other people’s developer journey smoother and more rewarding. If you noticed a mistake or have suggestions on how to improve this article, please let me know. If you think this article can be useful to others, please consider sharing it:

Stay in touch

© 2020-2024 Massimiliano Mirra