Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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: Backend struct implementing LanguageServer. Single file because tower-lsp-server’s macro, like rmcp’s, wants one impl block.
  • src/cli/lsp.rs: syn lsp subcommand. 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 six EntityType variants.
    • Inside tags: [<cursor>: distinct tags from the workspace.
  • 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 via file:// URI.
  • textDocument/publishDiagnostics: on didOpen, didChange, didSave: dangling [[slug]] references are flagged as Warning with source: "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 of tokio. 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 in mcp lsp builds.

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 for lsp-types security 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).