https://github.com/aoughwl/nimony-lsp
I built a Language Server Protocol implementation for Nimony — Araq's NIF-based reimplementation of the Nim compiler — plus a full VS Code extension. Instead of writing a parallel analysis engine, it turns the compiler's own outputs into the protocol every editor already speaks: navigation from the idetools backend, diagnostics from nimony check, and outline / hover / completion by reading the nimcache/*.s.nif artifacts in-process through Nimony's own NIF libraries. One static Nim binary speaks JSON-RPC over stdio; the extension is a thin vscode-languageclient wrapper.
Nimony already ships a real IDE backend (idetools) and emits a fully typed, lowered representation of every module into nimcache/. Nothing consumed either from an editor. Because the typed NIF is read directly, symbols, hover and completion reflect what the compiler actually resolved — not a regex approximation of source text. The whole server is ~2k lines of Nim on top of the reusable libraries under nimony/src/lib (nifstreams, nifcursors, nifindexes, symparser, lineinfos) — which, notably, import cleanly into a stock Nim 2 program.
Every capability below was driven end-to-end over real JSON-RPC against the compiled binary.
| Capability | Backing mechanism |
|---|---|
| Diagnostics (+ related info | )``nimony check`` stdout parsing; Trace -> relatedInfo |
| Go to definition | nimony check --def (idetools) |
| Find references | nimony check --usages (idetools), deduplicated |
| Hover | in-process NIF resolution -> multi-line sig + doc comment |
| Document symbols | .s.nif top-level walk; type nodes carry field kids |
| Completion | module + imported .s.idx.nif exports |
| Member completion (.) | fields + UFCS methods, resolved on the live buffer |
| Signature help | enclosing-call parse -> idetools -> params + active index |
| Document highlight | idetools occurrences in-file, read/write classified |
| Rename (+ prepareRename) | idetools references -> cross-file WorkspaceEdit |
| Workspace symbol | name search across every .s.nif in nimcache |
| Semantic tokens (full) | two-pass NIF walk -> typed legend, delta-encoded |
| Inlay hints | inferred-type hints for un-annotated let/var/const |
| Syntax highlighting | TextMate grammar (source.nimony) |
Cleanly layered so each concern is independently testable:
server/src/
nimony_lsp.nim stdio loop + dispatch + doc lifecycle
lsp/{jsonrpc,protocol,uris}.nim framing, wire types, file:// <-> path
server/{state,documents}.nim config, open-doc registry, UTF-16<->offset
driver/
nimonycli.nim run `nimony check [...]` + generation cache
diagnostics.nim `path(line,col) Kind: msg` -> Diagnostic[]
idetools.nim def/use tab records -> Location[]
nifindex.nim in-process .s.nif/.idx.nif -> symbols/hover/completion
signature/highlight/rename/workspacesym/semtokens/inlay.nim
The driver/* layer is the only code that touches Nimony. Everything that reads NIF is wrapped defensively: a malformed or missing artifact yields an empty result rather than taking the server down.
These cost the most debugging time and may help anyone else driving the compiler for tooling:
Member completion on an unsaved buffer. Nimony has no dirty-file/stdin mode, and a mid-edit origin. line does not parse, so no .s.nif exists. The server repairs the line (drops the trailing .), writes a throwaway temp module, compiles that to resolve the base identifier's type, reads its fields + any top-level routine whose first parameter is that type (UFCS), then deletes the temp and the exact nimcache artifacts it produced (snapshot-and-diff, so completion doesn't grow nimcache on every keystroke).
Generation-based check cache. A single editor request can trigger several whole-project checks (completion, then documentSymbols, then imported-index reads). nimonycli memoizes (sub, file, track) -> CheckResult keyed by a generation counter; the dispatch bumps the generation on any document lifecycle event, so redundant checks within one request collapse to a single compiler run while results never go stale relative to what the drivers can see.
Server:
cd server && nimble build # -> server/bin/nimony-lspconfig.nims adds nimony/src/lib to the path so the server links the compiler's NIF libraries. At runtime it needs a nimony binary (defaults to ~/nimony/bin/nimony; override via nimony.nimonyPath / NIMONY_EXE / initializationOptions).
VS Code extension:
cd client && npm install && npm run compile # -> client/out/extension.js
# press F5 for an Extension Development Host, or `vsce package` for a .vsixAny LSP-capable editor works: launch server/bin/nimony-lsp, talk JSON-RPC over stdio, pass the compiler path in initializationOptions.nimonyPath. Note Nim and Nimony share the .nim extension, so run this for Nimony projects and a Nim server for Nim projects — not both over the same files.
The next milestone is responsiveness, not more features:
Feedback, issues and PRs welcome — especially from anyone hacking on Nimony who can tell me where the idetools / NIF contracts are heading, so the server can track them. Would also love to hear if the in-process-NIF approach is the right long-term bet versus a persistent index served straight from the compiler.