ADR-018: LSP companion via tower-lsp-server
Date: 2026-05-25 Status: Accepted Author: Rômulo Febasi
Context
Adoption blocker for Synapse outside the dedicated syn terminal: the
user is editing markdown in Cursor / VS Code / Helix / Neovim / Zed
and has to switch out to syn search foo to find the right slug
before writing [[foo]]. Anything that reduces that friction, inline completion, hover, jump-to-definition: pays back immediately.
Two design vectors mattered for the v0.4 choice:
- One binary, every editor. A per-editor plugin would have to be maintained for each marketplace, learn each editor’s extension shape, and re-implement the slug index against the same data. The Language Server Protocol short-circuits all of that: a single binary that speaks LSP plugs into Cursor, VS Code, Helix, Neovim, Sublime, Kate, and Zed with a 4-line client config per editor.
- Reuse the local index. The MCP server reads from
.synapse/index.db; the LSP server reads from the same place. No new data path, no schema duplication.
rmcp provides MCP; the LSP equivalent is tower-lsp. The catch:
tower-lsp last released 0.20.0 on 2023-08-11 and has been
inactive ever since. The active fork is tower-lsp-server, currently
0.23.0 (released 2025-12-07, ~335k recent downloads on
crates.io). The fork tracks LSP 3.18 via lsp-types 0.98.
Decision
Adopt tower-lsp-server 0.23 behind a new lsp Cargo feature
(off by default), expose the server as syn lsp reading + writing
LSP over stdio.
Architecture.
src/lsp/mod.rs: module documentation, re-export.src/lsp/server.rs:Backendstruct implementingLanguageServer. Single file because tower-lsp-server’s macro, likermcp’s, wants oneimplblock.src/cli/lsp.rs:syn lspsubcommand. Workspace is optional, the LSP still serves type/tag completions on a brand-new workspace where the index does not yet exist.
Surface (v0.4).
textDocument/completion: three contextual lists:- Inside
[[<cursor>(wikilink): every entity slug, ranked by substring match against the partial query. - After
type: <cursor>: the sixEntityTypevariants. - Inside
tags: [<cursor>: distinct tags from the workspace.
- Inside
textDocument/hover: over a[[slug]]: bold title, type/id reference, first paragraph of the body.textDocument/definition: over a[[slug]]: jump to the entity’s markdown file viafile://URI.textDocument/publishDiagnostics: ondidOpen,didChange,didSave: dangling[[slug]]references are flagged as Warning withsource: "synapse".
Editor integration. Documented in
docs/EDITOR-INTEGRATION.md with copy-paste configs for Helix,
Neovim (nvim-lspconfig), VS Code (with a thin “Synapse” extension
stub), and Zed.
Why a separate feature (and not on by default).
- LSP transitively pulls
tower-lsp-server+ parts oftokio. Users who only want the CLI shouldn’t pay the link-time cost. - The MCP feature already gates
tokio; LSP shares the runtime when both are on, so it costs nothing extra inmcp lspbuilds.
Consequences
- The LSP server is local-first and offline by construction: same invariant as the CLI. No tokens, no network.
- Cross-editor consistency: every editor sees the same completions, the same hover, the same diagnostics. New editors that speak LSP work out of the box.
- The crate dep is a tower-lsp fork. That carries the risk that the fork stops being maintained: the mitigation is that our Backend is ~400 lines and could in extremis be ported to a hand-rolled JSON-RPC layer in a few days.
- We chose tower-lsp-server over the original tower-lsp despite the
Anthropic-bias-toward-canonical-crates because the canonical crate
has had no releases in nearly three years. The user’s
modern-stable-stack preference (memory
feedback_modern_stable) applies: pick the maintained fork over the abandoned upstream.
Alternatives considered
tower-lsp 0.20(original crate). Rejected: inactive since 2023-08-11, no LSP 3.18 support, no fix forlsp-typessecurity advisories shipped after the freeze. The fork (tower-lsp-server) is the active continuation.- Hand-rolled JSON-RPC over stdio. Doable (~500 lines) but duplicates well-tested code in the ecosystem and forces us to own Content-Length framing, async I/O, and the LSP schema mapping. Reserved as the fallback if the fork goes inactive too.
- Per-editor plugins (VS Code extension + Neovim plugin + …). Rejected: N editors × M maintenance cost. LSP is the standard exactly because the industry already paid that price.
- Server-side MCP-only (no LSP). Considered: MCP already exposes entities as resources. Rejected because MCP clients are not editors; the user editing markdown does not have an MCP client attached to that buffer.
- Web-based editor (Synapse Web). Out of scope for v0.4 and arguably out of scope for Synapse at all (local-first is core).