This is a proof-of-concept (POC) project. Its primary purpose is to research the feasibility of building a D language server directly on top of the dmd frontend, using dmd's semantic analysis as a library.
The ultimate goal is not to ship this POC as-is, but to use the results and experience gained here to produce a cleaned-up, production-ready implementation suitable for inclusion in dmd itself.
If you are looking for something more stable, check out my other LSP project: dls.
A minimal, dmd-frontend-native language server for D. Diagnostics, completion,
signature help and lint run on real dmd semantic, used read-only as a
library — no DCD, no libdparse heuristics.
- Complete language support: the server runs dmd's own semantic
analysis, so templates,
mixin, CTFE,__traitsand everything else are understood exactly as the compiler sees them. - Diagnostics: real dmd errors, plus lint for unused imports and unused parameters.
- Completion: semantic types, locals and imports.
- Navigation: goto definition/declaration/type definition/implementation, references, document highlight, call and type hierarchy.
- Signature help: parameters of the call under the cursor.
- Hover: type and docs.
- Rename: across the workspace.
- Inlay hints: inferred
autotypes and parameter names (opt-in). - Code actions: remove unused import(s), import a symbol from another module, implement/override members.
- Semantic tokens: identifier-level highlighting.
Supported LSP methods:
- lifecycle:
initialize,initialized,shutdown,exit - sync:
textDocument/didOpen,didChange,didClose,didSave - completion:
textDocument/completion - signature help:
textDocument/signatureHelp - navigation:
textDocument/definition,declaration,typeDefinition,implementation,references,documentHighlight - call hierarchy:
textDocument/prepareCallHierarchy,callHierarchy/incomingCalls,outgoingCalls - type hierarchy:
textDocument/prepareTypeHierarchy,typeHierarchy/supertypes,subtypes - rename:
textDocument/prepareRename,textDocument/rename - symbols:
textDocument/documentSymbol,workspace/symbol - hover:
textDocument/hover(type + docs) - code action:
textDocument/codeAction - inlay hints:
textDocument/inlayHint(opt-in) - folding / links:
textDocument/foldingRange,textDocument/documentLink - semantic highlighting:
textDocument/semanticTokens/full - diagnostics:
textDocument/publishDiagnostics - workspace:
workspace/didChangeConfiguration,didChangeWatchedFiles
make # -> ./dmd-lsp, from the vendored src/dmd/
make DC=ldmd2 # same, with LDC's dmd-compatible driver (smaller binary)
make check # struct-only guard + batch fixtures + LSP regression suite
make vscode # compile the VS Code extension (tsc + esbuild)
make vsix # package it -> editor/vscode/dmd-lsp.vsixNeeds a D compiler (DMD 2.113 line, or LDC ≥ 1.42 whose frontend covers it).
The dmd frontend is vendored under src/dmd/ so the build is self-contained;
refresh it with make vendor — see docs/vendoring.md.
Install dmd-lsp.vsix from the
nightly release
(or build it with make vsix). On first activation the extension downloads the
server for your platform and keeps it updated. Settings:
dmdLsp.serverPath: use a local build instead of the downloaded binary.dmdLsp.autoUpdate: re-download when the nightly changes (defaulttrue).- dmd-lsp: Create dls.json: write a starter config (see Configuration).
Download the standalone binary for your platform from the
nightly release
(with SHA256SUMS), or build it above:
dmd-lsp-linux-x64.tar.gzdmd-lsp-darwin-arm64.tar.gzdmd-lsp-darwin-x64.tar.gzdmd-lsp-win-x64.zip(dmd-lsp.exe)
Then point any LSP client at dmd-lsp over stdio (see Features).
./dmd-lsp --check FILE... # batch diagnostics (exit 1 on errors)
./dmd-lsp --stdio # LSP over stdio (default)
./dmd-lsp --import=DIR... # module import paths (-I)
./dmd-lsp --string-import=DIR... # string import paths (-J, import("..."))
./dmd-lsp --flag=-preview=NAME # pass a supported dmd flag
./dmd-lsp --debounce-ms=N # diagnostics idle delay (default 500)
./dmd-lsp --versionSet DMD_LSP_TIMING=1 to print phase timings (discovery, index build,
analysis, workspace-wide references) to stderr, and DMD_LSP_DEBUG=1 to log
every module the server parses or analyses, with the level that holds it
(debug: parse & analyse module: 'src/app.d' (overlay)).
Precedence: editor settings → CLI → dls.json → builtin stdlib defaults.
Editor initializationOptions/settings take the same keys (bare, or under
"dmd-lsp"/"d"/"D"). Mirrored dmd flags matter: previews change overload
resolution, so a build that omits one here shows spurious errors. dls.json
loads at initialize and on save.
| Key | Default | Effect |
|---|---|---|
inlayHints |
false |
Show inferred auto types and call parameter names (requires a client that supports inlay hints). |
autoImports |
false |
Include symbols from other project modules in completion, with the import added as an edit. The explicit Import <name> from <module> code action is always available regardless. |
fullTypeHover |
true |
Show the full declaration body (with the name qualified, e.g. struct pkg.Name { … }) in hover for structs/classes/unions/enums. Set false for just the short qualified name (struct pkg.Name). |
dub projects (
dub.json/dub.sdl) aren't auto-configured yet — list the dependency import paths indls.jsonfor now;dub describesupport is planned.
dmd-lsp runs the real dmd frontend in the server process, on memory
levels (design.md):
- dmd: the frontend, configured (import paths, flags);
- dependencies: every module the open roots import, analysed once;
- warm: a renamed copy of the file being edited, analysed once, so the template instances it needs from libraries are already there;
- overlay: the file(s) being edited, analysed from the editor buffer.
Each level has its own heap (a private druntime GC instance). While a level is
on top, the levels below are write-protected; the first write dmd makes to one
of their pages saves a copy of the page. Popping the overlay copies those pages
back, restores dmd's globals and unmaps the overlay's heap, so the dependency
level is byte for byte what it was before the edit — no matter what dmd cached
along the way (template instances, interned types, lazily analysed functions).
An edit re-analyses only the overlay on the warm dependencies, and memory is
flat over any number of edits, root switches and rebuilds. It is fork() done
in-process and scoped to dmd, and works the same on Linux, macOS and Windows
(page protection via mprotect/VirtualProtect).
Analysis is debounce-only: a keystroke never builds. Completion, hover and the other read-only requests answer from the last analysis until the debounced pass replaces it. A failure inside dmd (an assert, or a segfault on broken code) fails that one request: the levels are dropped, which restores a clean frontend, and the server keeps going.
- design.md: internals, conventions, limitations, status
- vendoring.md: vendored dmd sources, Makefile
- releases.md: nightly CI, VS Code extension
- upstream.md: patches in
../dmd, roadmap - findings.md: memory/GC investigation, and how it ended
- reclamation.md: the memory-level invariants + vendor-update checklist