Emacs beyond the basics, part III: a real programming setup
Emacs has been a programming editor since before most programming languages we use today existed. For a while, though, setting it up for modern work meant a pile of third-party packages and a lot of patience. That changed. Since Emacs 29, the core pieces of a modern setup come in the box: a client for language servers, tree-sitter parsing and use-package, next to the project management that was already there. Emacs 30 and 31 filled in a lot of the gaps.
This post turns a plain Emacs into a comfortable place to write code. It assumes you've read the first Emacs guide, or at least that C-x C-f and M-x don't scare you anymore. Part II helps if you want to understand every line of the config, but you can follow along without it.
Everything here works on Emacs 29 and newer, unless I point out a version.
Projects #
Most of programming happens inside a project, usually a Git repository. Emacs has project.el built in, and it figures out the project on its own: open any file inside a Git repository and Emacs knows where the project starts.
Everything lives under C-x p:
C-x p p: switch to another project
C-x p f: find a file in this project
C-x p g: search the whole project for a regexp
C-x p r: search and replace across the project
C-x p b: switch to a buffer from this project
C-x p D: open Dired at the root of the project
C-x p d: pick a folder inside the project and open it in Dired
C-x p s: open a shell at the root of the project
C-x p e: open Eshell at the root of the project
C-x p c: compile the project
C-x p !: run a shell command at the root
C-x p k: close every buffer of this project
C-x p p remembers every project you've opened. Pick one and Emacs asks what you want to do there: find a file, search, open a shell, and so on. Switching between three projects becomes a matter of a few keys.
If a folder isn't a Git repository but you still want Emacs to treat it as a project, you can teach it other markers, like a package.json or a requirements.txt, through the project-vc-extra-root-markers option.
Language servers with Eglot #
A language server is a program that understands one language deeply: where every function is defined, what a variable's type is, what's wrong with the line you just wrote. VS Code, Neovim and Emacs all talk to the same servers, using the same protocol. Emacs's client is called Eglot, and it's built in since version 29.
First, install the server for your language. Some common ones:
npm install -g pyright # Python
go install golang.org/x/tools/gopls@latest # Go
rustup component add rust-analyzer # Rust
npm install -g typescript-language-server typescript # JavaScript and TypeScript
sudo apt install clangd # C and C++, or your system's package manager
Then open a file and run M-x eglot. Eglot already knows which server goes with which language, so most of the time there's nothing to configure. For Python alone it knows about a long list of servers, from pyright and basedpyright to pylsp and ruff, and it picks the first one it finds installed.
From then on, these keys do the heavy lifting:
M-.: go to the definition of what's under the cursor
M-,: come back
M-?: find every place that uses it
M-x eglot-rename: rename it everywhere
M-x eglot-code-actions: quick fixes the server suggests
M-x eglot-format-buffer: format the file
M-x flymake-show-buffer-diagnostics: list every error and warning
The errors appear underlined as you type, through Flymake, which is also built in. The documentation of the thing under the cursor shows up at the bottom of the screen, through ElDoc. Neither needs setup.
To start Eglot automatically, add it to the hooks of the modes you use:
(use-package eglot
:hook ((python-ts-mode go-ts-mode rust-ts-mode) . eglot-ensure)
:bind (:map eglot-mode-map
("C-c e r" . eglot-rename)
("C-c e a" . eglot-code-actions)
("C-c e f" . eglot-format-buffer)))
The :bind part gives the three most useful commands short keys, under C-c e, only when Eglot is active. And if your server isn't on Eglot's list, you add it yourself:
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(lua-mode . ("lua-language-server"))))
Tree-sitter: syntax that understands the code #
Old Emacs modes highlighted code with regular expressions, which works most of the time and breaks in strange ways the rest. Tree-sitter parses the file into a real syntax tree, like a compiler would. The result is faster, more accurate highlighting and indentation, and smarter movement through the code.
Each language has a mode with -ts- in the name: python-ts-mode, go-ts-mode, rust-ts-mode, typescript-ts-mode, tsx-ts-mode, js-ts-mode, json-ts-mode, bash-ts-mode, yaml-ts-mode and many more. Each one needs a grammar, a small library for that language.
On Emacs 31, this got a lot easier. One line tells Emacs to prefer the tree-sitter modes:
(setopt treesit-enabled-modes t)
When a grammar is missing, Emacs 31 asks if you want to install it, and does the work for you. That's the default behavior, controlled by the treesit-auto-install-grammar option. Building a grammar needs a C compiler on your system, so have gcc or clang around.
On Emacs 29 and 30, it takes two steps. Install the grammar with M-x treesit-install-language-grammar, then tell Emacs to use the new mode instead of the old one:
(add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))
People on those versions often use a package from MELPA called treesit-auto to automate this. On Emacs 31 you don't need it anymore.
Completion while you type #
Completion in the minibuffer was covered in the first guide, with Vertico, Orderless and Marginalia. Completion inside the code is a separate thing, and there are two good paths.
The built-in one, since Emacs 30, is completion-preview-mode. It shows the most likely completion in gray right after the cursor, and TAB accepts it. It's quiet and light.
If you prefer the classic popup with a list of options, use Corfu, from GNU ELPA:
(use-package corfu
:ensure t
:custom (corfu-auto t)
:init (global-corfu-mode))
With corfu-auto, the list shows up on its own while you type. Eglot feeds it, so you get the same completions as any other editor using that language server.
Finding things #
Programming is mostly reading, and reading is mostly searching. Consult, from GNU ELPA, gives Emacs a set of search commands with live previews:
(use-package consult
:ensure t
:bind (("C-x b" . consult-buffer)
("M-s l" . consult-line)
("M-s r" . consult-ripgrep)
("M-g i" . consult-imenu)))
consult-bufferreplaces the buffer switcher and also lists recent files and bookmarks.consult-linesearches the lines of the current file and jumps as you move through the results.consult-ripgrepsearches the whole project with ripgrep, so installrgfirst.consult-imenulists every function, class and heading in the file. Without Consult, the built-inM-x imenudoes the same in a simpler way.
Embark, also from GNU ELPA, is the companion. Press its key on anything, a file in a list, a symbol in the code, a search result, and it shows what you can do with it: open, rename, copy, delete, search for it. If you use Consult and Embark together, also install embark-consult, which connects the two.
(use-package embark
:ensure t
:bind ("C-." . embark-act))
C-. works in graphical Emacs. In a terminal, pick another key.
One gem in Emacs 31: search results became editable. Run M-x rgrep or M-x grep, press e in the results buffer, and edit the lines right there. Your changes go straight into the files. C-c C-c finishes. Renaming a word across forty files becomes a search and a couple of keystrokes.
Compile, test and jump to the errors #
M-x compile runs any command, make, cargo build, go test ./..., npm test, and shows the output in a buffer. Error messages become links: press RET on one and Emacs opens the file at that line. C-x p c does the same from the root of the project.
M-x compile: run a build or test command
M-x recompile: run the same command again
M-g n: jump to the next error
M-g p: jump to the previous error
C-x followed by the backquote key also jumps to the next error. Set compilation-scroll-output to 'first-error and the output stops scrolling at the first problem, which is usually the one you want to read.
Running code interactively #
Many languages have a REPL integration. Python is a good example of how they work:
C-c C-p: start a Python shell (run-python)
C-c C-c: send the whole file to it
C-c C-z: jump between the code and the shell
The same idea exists for Lisp, Ruby, Scheme, shell scripts and others. Write some code, send it, look at the result, adjust. It's a different rhythm from save and run, and a pleasant one.
Debugging #
For C, C++ and other languages GDB understands, Emacs has a full interface built in. Run M-x gdb, then M-x gdb-many-windows, and you get the source, the variables, the stack, the breakpoints and the GDB console laid out together.
For everything else, there's Dape, from GNU ELPA. It speaks the Debug Adapter Protocol, the same one VS Code uses for its debugger, so it works with debugpy for Python, delve for Go, codelldb for Rust and C++, and the JavaScript debuggers. Install the adapter for your language, run M-x dape and choose a configuration.
Git, besides Magit #
Magit is still the star, and the first guide covers it. But Emacs also has a built-in version control interface, VC, which works with Git, Mercurial and others, and has quick keys for small jobs:
C-x v v: do the next logical thing (add a new file, commit changes)
C-x v =: diff of the current file
C-x v D: diff of the whole repository
C-x v l: log of the current file
C-x v g: who wrote each line (blame)
When a merge goes wrong, open the file with conflicts and Emacs turns on smerge-mode by itself. Its commands live under C-c ^: C-c ^ n goes to the next conflict, C-c ^ u keeps your version (the upper one), C-c ^ l keeps the other one (the lower one) and C-c ^ a keeps both.
And for a quiet reminder of what you've changed, diff-hl marks changed lines in the margin:
(use-package diff-hl
:ensure t
:init (global-diff-hl-mode))
Small helpers that add up #
A few built-in modes that are worth turning on in code buffers:
electric-pair-modecloses brackets and quotes for you.subword-modemakesM-fandM-bstop insidecamelCasewords.hs-minor-modefolds functions and blocks. Its keys live underC-c @, andC-c @ C-hhides the block you're in.which-function-modeshows the name of the current function in the mode line.whitespace-modeshows tabs and trailing spaces, for when the linter complains about something invisible.display-line-numbers-mode, withdisplay-line-numbers-typeset to'relativeif you like counting jumps.editorconfig-mode, built in since Emacs 30, follows the.editorconfigfile that many projects have, so your indentation matches the team's.
Turning them on for every programming mode is one hook each:
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'prog-mode-hook #'hs-minor-mode)
(add-hook 'prog-mode-hook #'subword-mode)
(add-hook 'prog-mode-hook #'electric-pair-local-mode)
(editorconfig-mode 1) ; Emacs 30 and newer
For snippets, the classic package is YASnippet, from GNU ELPA. Emacs also ships simpler tools for the same job, like abbrevs: M-x define-global-abbrev, type a short word and its expansion, and it expands as you type once abbrev-mode is on.
Code inside containers #
TRAMP, which the first guide used for SSH, also opens files inside containers since Emacs 29:
/docker:container-name:/app/main.py
/podman:container-name:/app/main.py
Dired, shells and M-x compile all work in those paths, so you can browse and build inside a container without leaving Emacs.
Putting it together #
Here's the programming part of a config, in one block, ready to paste after the config from the first guide:
;;; Programming
(use-package eglot
:hook ((python-ts-mode go-ts-mode rust-ts-mode) . eglot-ensure)
:bind (:map eglot-mode-map
("C-c e r" . eglot-rename)
("C-c e a" . eglot-code-actions)
("C-c e f" . eglot-format-buffer)))
(use-package corfu
:ensure t
:custom (corfu-auto t)
:init (global-corfu-mode))
(use-package consult
:ensure t
:bind (("C-x b" . consult-buffer)
("M-s l" . consult-line)
("M-s r" . consult-ripgrep)
("M-g i" . consult-imenu)))
(use-package embark
:ensure t
:bind ("C-." . embark-act))
(use-package embark-consult
:ensure t)
(use-package diff-hl
:ensure t
:init (global-diff-hl-mode))
(setq compilation-scroll-output 'first-error)
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'prog-mode-hook #'hs-minor-mode)
(add-hook 'prog-mode-hook #'subword-mode)
(add-hook 'prog-mode-hook #'electric-pair-local-mode)
;; Emacs 30 and newer
(editorconfig-mode 1)
;; Emacs 31: prefer tree-sitter modes and offer to install grammars
(setopt treesit-enabled-modes t)
On Emacs 29, remove the editorconfig line. On Emacs 29 and 30, remove the tree-sitter line at the end and use major-mode-remap-alist for tree-sitter, as shown above. As always, select the block and press TAB to let Emacs indent it nicely.
A sane way to adopt all this #
Don't paste everything on a Monday morning before a deadline. Add one piece at a time, in this order: projects, then Eglot, then the search commands, then the rest. Live with each one for a few days. The pieces you actually use will stick, and the ones you don't can go.
This is part III of a series. Part I is about living inside Emacs: feeds, gopher, the fediverse, chat and mail. Part II teaches enough Emacs Lisp to bend all of this to your taste.