Language servers (LSP)
Macros speaks the Language Server Protocol. The protocol layer — sending requests, routing responses, jumping to locations, applying edits — is in Rust; the user-facing commands, the result-buffer keymaps, and the default keys are wired in Scheme, so you can rebind and extend everything.
Setting up a language server
Language servers are opt-in: none are configured out of the box (the one exception is the built-in, in-process Scheme server, which needs no binary). You enable a language by declaring it with the lsp-language form in your init.scm; the server then starts lazily the first time you open a matching file — provided the server binary is on your PATH. (Syntax highlighting is separate — it comes from the bundled tree-sitter grammars and is always on, with or without a server.)
The smallest useful example — Rust, via rust-analyzer, formatting on save:
(lsp-language "rust"
'extensions '("rs")
'command "rust-analyzer"
'format-on-save 'lsp) ; format through the server on save
Enabling the common servers
Paste any of these into your init.scm and keep the ones whose server binaries are installed and on your PATH. This is the set Macros used to configure automatically; format-on-save 'lsp uses each server's own formatter (rustfmt, gofmt, clang-format, …) — no extra binary, and a harmless no-op if the server can't format.
(lsp-language "rust" 'extensions '("rs") 'command "rust-analyzer" 'format-on-save 'lsp)
(lsp-language "go" 'extensions '("go") 'command "gopls" 'format-on-save 'lsp)
(lsp-language "c" 'extensions '("c" "h") 'command "clangd" 'format-on-save 'lsp)
(lsp-language "cpp" 'extensions '("cpp" "cc" "hpp") 'command "clangd" 'format-on-save 'lsp)
(lsp-language "python" 'extensions '("py") 'command "pylsp" 'format-on-save 'lsp)
;; .ts and .tsx share one server but must announce different languageIds, or a
;; .tsx file opens as plain "typescript" and every JSX element is a syntax error.
(lsp-language "typescript"
'extensions '("ts" "tsx")
'language-ids '(("ts" . "typescript") ("tsx" . "typescriptreact"))
'command "typescript-language-server --stdio"
'format-on-save 'lsp)
(lsp-language "javascript"
'extensions '("js" "jsx")
'language-ids '(("js" . "javascript") ("jsx" . "javascriptreact"))
'command "typescript-language-server --stdio"
'format-on-save 'lsp)
;; JVM languages (highlighting is bundled; install the server to enable LSP):
(lsp-language "scala" 'extensions '("scala" "sc" "sbt") 'command "metals" 'format-on-save 'lsp)
(lsp-language "clojure" 'extensions '("clj" "cljs" "cljc" "edn") 'command "clojure-lsp" 'format-on-save 'lsp)
The same block lives (commented) in the DEFAULT LANGUAGE CONFIGURATION section at the end of scheme/lsp.scm.
'command is the server invocation; put any flags right in the string:
(lsp-language "typescript"
'extensions '("ts" "tsx")
'language-ids '(("tsx" . "typescriptreact")) ; per-extension wire id
'command "typescript-language-server --stdio")
Options
| Option | What it does |
|---|---|
'extensions |
File extensions that activate this language (a list) |
'command |
The server command line — flags allowed inline |
'format-on-save |
'lsp (format via the server), a shell formatter like (list "black" "-"), or the name of your own Scheme function |
'inlay-hints |
On by default; pass #f to turn the type/parameter hints off |
'semantic-tokens |
#t to color from the server's semantic tokens — turn rainbow delimiters off for that language so they don't fight over the overlay layer |
'code-lens |
#t to show code lenses |
'language-ids |
Per-extension languageId overrides, an alist of (ext . id) |
More examples
;; Python, formatted by black instead of the server ("-" makes black read stdin):
(lsp-language "python"
'extensions '("py")
'command "pylsp"
'format-on-save (list "black" "-"))
;; Go with inlay hints and semantic-token coloring on:
(lsp-language "go"
'extensions '("go")
'command "gopls"
'format-on-save 'lsp
'inlay-hints #t
'semantic-tokens #t)
;; Zig via zls, with inlay hints off:
(lsp-language "zig"
'extensions '("zig")
'command "zls"
'format-on-save 'lsp
'inlay-hints #f)
Declaring a language is idempotent — re-declaring it (e.g. to flip 'inlay-hints) just replaces the previous definition, so you can refine a config by stating it again later in your init.scm.
The built-in Scheme server
The in-process Scheme server needs no binary and runs automatically for .scm files. Editor config and extension code (anything under ~/.config/macros, or a *.macros.scm file) get the macros-scheme variant, which additionally seeds the whole editor API. It provides completion, "unknown function" diagnostics, go-to-definition (M-.), find-references (M-?), hover (C-c C-l h), and rename (C-c C-l n).
Because command/keybinding references are 'symbols rather than strings, the server sees them as real identifiers: M-. on 'magit-status in a keybinding jumps to its (define …) (in your config or the bundled source), M-? lists its uses — quoted references included — and hover shows its docs. The leading quote is a word boundary, so the bare name is what resolves.
Rename is scoped to the current document and to symbols it defines — renaming a command you wrote in init.scm updates its definition, keybindings, and calls there (quoted references included). A symbol defined elsewhere (a bundled command, another file) is declined ("Cannot rename this symbol"), since the built-in server can't safely update across files. (In config buffers, C-c C-d / C-c C-f also give doc-at-point via the Scheme-side macros-eldoc / macros-describe-point — see Scripting.)
Navigation & refactoring commands
All are M-x-discoverable. The most common two have short bindings; the rest live under the C-c C-l prefix.
| Key | Command | Action |
|---|---|---|
| M-. | lsp-find-definition |
Go to definition |
| M-? | lsp-find-references |
List all references |
| M-* | xref-pop-marker |
Jump back |
| C-c C-d | lsp-hover |
Documentation at point |
| C-c C-l d | lsp-find-definition |
Definition |
| C-c C-l D | lsp-find-declaration |
Declaration |
| C-c C-l t | lsp-find-type-definition |
Type definition |
| C-c C-l i | lsp-find-implementation |
Implementation(s) |
| C-c C-l r | lsp-find-references |
References |
| C-c C-l n | lsp-rename |
Rename everywhere |
| C-c C-l h | lsp-hover |
Hover docs |
| C-c C-l k | lsp-signature-help |
Signature of the call at point |
| C-c C-l s | lsp-document-symbols |
Symbols in this buffer |
| C-c C-l S | lsp-workspace-symbol |
Query symbols across the project |
| C-c C-l f | lsp-format-buffer |
Format the buffer |
| C-c C-l a | lsp-code-action |
Code actions / quick fixes |
More under C-c C-l
| Key | Command |
|---|---|
| C-c C-l H | lsp-document-highlight |
| C-c C-l I | lsp-inlay-hints |
| C-c C-l L | lsp-code-lens |
| C-c C-l e / C-c C-l E | lsp-expand-selection / lsp-contract-selection |
| C-c C-l z | lsp-fold-all |
| C-c C-l c / C-c C-l C | call hierarchy incoming / outgoing |
| C-c C-l T | lsp-semantic-tokens-apply |
| C-c C-l o | lsp-open-link |
Result buffers
References and symbol queries open read-only list buffers (*lsp-xref*, *lsp-symbols*). They behave like every list buffer in Macros:
| Key | Action |
|---|---|
| Enter | visit the target for the current row |
| n / p | move down / up |
| q | close |
Hover docs open in an *lsp-doc* buffer (q to dismiss). Code actions open an actions list where Enter applies the selected action.
Rename
lsp-rename validates the position with prepareRename, prompts you (pre-filled with the current name), and applies the resulting workspace edit across every file.
Logging
The language server's stderr is captured. View it with C-h l (list-lsp-log). To log server startup, set:
(set-option "lsp-log-on-start" #t)
Diagnostics & semantic tokens
LSP diagnostics drive the diagnostics list and gutter dots. Semantic-token highlighting shares the overlay-face layer with rainbow delimiters — enable one per buffer at a time so they don't fight over that layer.