Language server (elm lsp)

elm lsp runs a Language Server Protocol server for Elm over stdio. It speaks JSON-RPC 2.0 with the usual Content-Length-framed messages, so any LSP-capable editor can drive it. The same binary backs the VS Code client — that extension is just a thin launcher.

./mvnw -DskipTests package      # build target/elm.jar
java -jar target/elm.jar lsp    # start the server on stdin/stdout

The server is single-process, in-memory, and dependency-free (no elm.json, no network, no index files on disk). It type-checks with the same Hindley–Milner inference the rest of the toolchain uses, so hovers, inlay hints and diagnostics are all driven by real inference rather than heuristics.

---

Capabilities at a glance

FeatureLSP methodNotes
DiagnosticstextDocument/publishDiagnosticsErrors and warnings, on open and on every change.
HovertextDocument/hoverInferred type of the definition under the cursor.
Go to definitiontextDocument/definitionResolves locally and across the workspace (and through qualified imports).
Find referencestextDocument/referencesWorkspace-wide.
RenametextDocument/renameRewrites every occurrence, across modules.
CompletiontextDocument/completionModule-local names + the bundled standard library, with inferred-type detail. After Module. offers that module's members; after a record. offers field names (trigger character .).
Document symbolstextDocument/documentSymbolOutline of a file's top-level declarations.
Workspace symbolsworkspace/symbolSearch top-level symbols across all indexed files (e.g. VS Code Ctrl+T).
Call hierarchytextDocument/prepareCallHierarchy, callHierarchy/incomingCalls, callHierarchy/outgoingCallsWho calls a function, and what it calls — across modules (qualified and unqualified).
Document highlighttextDocument/documentHighlightEvery occurrence of the symbol under the cursor in the file.
Code lensestextDocument/codeLensA reference count above each top-level definition.
Inlay hintstextDocument/inlayHintInferred type signatures shown inline for unannotated values.
Signature helptextDocument/signatureHelpThe called function's type while typing arguments.
Semantic tokenstextDocument/semanticTokens/fullKeyword / type / number / … classification for highlighting.
Code actionstextDocument/codeActionQuick-fixes and refactors (see below).
FormattingtextDocument/formattingWhole-document elm-format-style reformat.

Diagnostics

Published on didOpen and after each didChange. Each diagnostic carries source: "elm-lang" and a severity:

Code actions

Offered for the cursor's position / selection:

Quick-fixes (kind: "quickfix")

Refactors (kind: "refactor.extract")

textDocument/rangeFormatting ("Format Selection") is also supported — it reformats the whole module (the formatter operates on a complete module, not a fragment).

---

Editor integration

VS Code

Use the bundled client in editor/vscode/ — see its README. It launches java -jar elm.jar lsp, enables format-on-save, and exposes elmLang.serverJar / elmLang.javaPath / elmLang.trace.server settings.

Neovim (built-in LSP)

vim.api.nvim_create_autocmd("FileType", {
  pattern = "elm",
  callback = function(args)
    vim.lsp.start({
      name = "elm-lang",
      cmd = { "java", "-jar", "/abs/path/to/target/elm.jar", "lsp" },
      root_dir = vim.fs.dirname(vim.fs.find({ "elm.json", ".git" }, { upward = true })[1]),
    })
  end,
})

(There is no elm.json requirement; root_dir only scopes which .elm files get indexed for workspace-wide navigation.)

Any other client

Spawn java -jar elm.jar lsp, connect its stdin/stdout, and send a normal initialize handshake. Pass the workspace root as rootUri (or workspaceFolders) so the server can index every .elm file under it for workspace symbols, references and rename. The server advertises full textDocumentSync (send whole-document didChange updates).

---

Workspace indexing

On initialize, the server walks the rootUri / workspaceFolders directories and reads every .elm file into memory, so go-to-definition, find-references, rename and workspace-symbol search work before a file is opened. Opening or editing a file (didOpen / didChange) overrides the on-disk copy with the editor's in-memory buffer. Indexing is best-effort — unreadable trees are skipped.

---

Protocol notes

Trying it by hand

A minimal session (newlines shown for clarity — real messages are Content-Length-framed):

--> initialize            { "rootUri": "file:///abs/project" }
<-- (capabilities)
--> textDocument/didOpen   { "textDocument": { "uri": "file:///abs/project/Main.elm", "text": "…" } }
<-- textDocument/publishDiagnostics   { "uri": …, "diagnostics": [ … ] }
--> textDocument/hover     { "textDocument": { "uri": … }, "position": { "line": 2, "character": 4 } }
<-- (inferred type)

The VS Code client's elmLang.trace.server: "verbose" setting prints this traffic to its output channel, which is the easiest way to watch a real conversation.

Troubleshooting