LSP Service
- Entity ID:
ent-20260410-685fb5034293 - Type:
service - Scope:
shared - Status:
active - Aliases: LSP, Language Server Protocol, code intelligence, language server
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)
initializeLspServerManager()— creates singleton, starts async initialization. Skipped in--baremode.getLspServerManager()— returns the singleton or undefinedreinitializeLspServerManager()— force re-init (for plugin reload, fixes issue #15521)shutdownLspServerManager()— graceful cleanupisLspConnected()— checks if any server is not in error stategetInitializationStatus()— returns'not-started' | 'pending' | 'success' | 'failed'- Generation counter (
initializationGeneration) prevents stale promises from updating state
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)
MAX_DIAGNOSTICS_PER_FILE = 10MAX_TOTAL_DIAGNOSTICS = 30MAX_DELIVERED_FILES = 500(LRU dedup)
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
- Document sync (open/change/save/close)
textDocument/definition(viasendRequest)- Passive diagnostics (
textDocument/publishDiagnostics) - Protocol tracing
workspace/configurationhandling (returns null config)
Trade-offs
- 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.
- Passive diagnostics only — diagnostics are collected and delivered as attachments, not actively queried. The agent sees errors when they're published, not on demand.
- State machine per server — clean lifecycle management but adds complexity. Retry on error (3 attempts, 500ms base delay) helps with transient failures.
- Skipped in
--baremode — keeps headless/scripted usage fast but means no code intelligence in those contexts.
Depends on
vscode-jsonrpc— JSON-RPC over stdio- plugin-system — LSP server discovery
- hooks-system — PostFileWrite hooks trigger LSP notifications
Key claims
- LSP servers discovered exclusively through plugins — no built-in servers
- 4-layer architecture: manager, router, instance, client
- Passive diagnostic limits: 10 per file, 30 total, 500 delivered files (LRU)
- Retry on transient errors: 3 attempts with 500ms base delay
Relations
depends_onplugin-systempart_ofservice-layerrelated_tohooks-system
Sources
src-20260409-a5fc157bc756, source code analysis of src/services/lsp/