LSP Service

Description

The LSP service (src/services/lsp/) manages connections to Language Server Protocol servers that provide code intelligence features. It uses a 4-layer architecture: singleton manager, server routing by file extension, per-server instances with state machines, and raw JSON-RPC clients. LSP servers are discovered exclusively through plugins and provide passive diagnostics that are delivered as conversation attachments.

Architecture

Layer 1: Singleton manager (manager.ts)

Layer 2: Server routing (LSPServerManager.ts)

createLSPServerManager() factory function. Routes requests to the correct server by file extension via extensionMap: Map<string, string[]>.

Key methods: initialize(), shutdown(), getServerForFile(path), ensureServerStarted(path), sendRequest(path, method, params), openFile(), changeFile(), saveFile(), closeFile(), isFileOpen().

Tracks open files: openedFiles: Map<string, string> (URI -> server name).

LSP document sync methods: textDocument/didOpen, textDocument/didChange, textDocument/didSave, textDocument/didClose.

Layer 3: Server instances (LSPServerInstance.ts)

Per-server state machine: stopped -> starting -> running, running -> stopping -> stopped, any -> error, error -> starting (retry).

Constants: - LSP_ERROR_CONTENT_MODIFIED = -32801 - MAX_RETRIES_FOR_TRANSIENT_ERRORS = 3 - RETRY_BASE_DELAY_MS = 500

Config: ScopedLspServerConfig requires command and extensionToLanguage (maps file extension to language ID).

Layer 4: JSON-RPC client (LSPClient.ts)

createLSPClient(serverName, onCrash?) spawns the language server as a child process via spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).

Uses vscode-jsonrpc/node.js for StreamMessageReader + StreamMessageWriter to create a MessageConnection.

Lifecycle: start() -> initialize() -> ready. stop() sends shutdown request then exit notification.

Queues handlers registered before connection is ready (pendingHandlers[], pendingRequestHandlers[]). onCrash callback fires on non-zero exit code during operation (not during intentional stop).

Passive diagnostics

Collection (passiveFeedback.ts)

Registers textDocument/publishDiagnostics handlers on all servers. Converts LSP diagnostics to Claude format via formatDiagnosticsForAttachment().

LSP severity mapping: 1=Error, 2=Warning, 3=Information->Info, 4=Hint.

Storage (LSPDiagnosticRegistry.ts)

Pattern: receive diagnostics -> store -> check for new diagnostics -> get attachments -> deliver to conversation.

Server discovery

LSP servers are discovered exclusively through plugins via getAllLspServers(): 1. loadAllPluginsCacheOnly() — load plugin configs from cache 2. getPluginLspServers() — extract LSP server definitions from each plugin 3. Load in parallel per plugin, merge results

No built-in LSP servers — all come from plugin configuration.

Supported LSP features

Trade-offs

  1. Plugin-only discovery — no built-in LSP servers means zero config for users who don't use plugins, but also means LSP features are invisible to most users.
  2. Passive diagnostics only — diagnostics are collected and delivered as attachments, not actively queried. The agent sees errors when they're published, not on demand.
  3. State machine per server — clean lifecycle management but adds complexity. Retry on error (3 attempts, 500ms base delay) helps with transient failures.
  4. Skipped in --bare mode — keeps headless/scripted usage fast but means no code intelligence in those contexts.

Depends on

Key claims

Relations

Sources

src-20260409-a5fc157bc756, source code analysis of src/services/lsp/