LSP Service
The LSP service (src/services/lsp/) integrates Language Server Protocol servers into Claude Code, providing diagnostics, hover information, and go-to-definition capabilities. It manages multiple language servers and routes requests based on file extensions.
Architecture
The LSP service follows a two-layer architecture:
LSPServerManager(factory function): Routes requests to the correct language server based on file extensionmanager.ts(singleton): Manages the global lifecycle of theLSPServerManagerinstance
LSPServerManager
Created via the createLSPServerManager() factory function, which uses closures for state encapsulation:
export function createLSPServerManager(): LSPServerManager {
const servers: Map<string, LSPServerInstance> = new Map()
const extensionMap: Map<string, string[]> = new Map()
const openedFiles: Map<string, string> = new Map()
// ... methods exposed via return object
}Loads all configured LSP servers from plugins, builds the extension-to-server mapping, and creates server instances.
Returns the appropriate server instance for a given file path based on its extension.
Lazily starts the language server for a file if not already running.
Sends an LSP request (e.g., textDocument/definition) to the appropriate server.
Synchronize file lifecycle events with the LSP server via didOpen, didChange, didSave, and didClose notifications.
Supported Languages
LSP servers are loaded exclusively from plugins. Each server declares which file extensions it handles via the extensionToLanguage mapping:
| Language | Typical Extensions | Server |
|---|---|---|
| TypeScript / JavaScript | .ts, .tsx, .js, .jsx | TypeScript Language Server |
| Python | .py | Pyright / Pylsp |
| Go | .go | gopls |
| Rust | .rs | rust-analyzer |
Language servers are provided by plugins, not built into the CLI. The set of supported languages depends on which plugins are installed and enabled.
File Extension Routing
When a request arrives for a file, the manager:
- Extracts the file extension from the path
- Looks up the extension in the
extensionMapto find the server name - Retrieves the
LSPServerInstancefrom theserversmap - Lazily starts the server if it has not been initialized yet
const instance = manager.getServerForFile('/project/src/index.ts')
// Returns the TypeScript LSP server instanceInitialization States
The singleton manager in manager.ts tracks initialization through a state machine:
not-started --> pending --> success
\--> failedManager has not been created. Returned when initializeLspServerManager() has never been called (e.g., headless subcommands).
Manager created, async initialization in progress. The initializationPromise can be awaited.
Initialization completed successfully. Passive notification handlers are registered.
Initialization failed. The manager instance is cleared. A subsequent call to initializeLspServerManager() will retry.
Generation Counter
A initializationGeneration counter prevents stale initialization promises from updating state when re-initialization is triggered:
const currentGeneration = ++initializationGeneration
lspManagerInstance.initialize()
.then(() => {
if (currentGeneration === initializationGeneration) {
initializationState = 'success'
}
})Lazy Server Startup
Language servers are created during initialize() but not started until first use. The ensureServerStarted() method triggers the actual process spawn and LSP handshake on demand:
// Server created but not running
await manager.initialize()
// First file access triggers server startup
const server = await manager.ensureServerStarted('/project/src/app.ts')This avoids spawning language server processes for languages the user never touches during a session.
Diagnostic Registry
When initialization succeeds, the manager registers passive notification handlers via registerLSPNotificationHandlers(). These handlers listen for textDocument/publishDiagnostics notifications from language servers and feed them into Claude Code's diagnostic tracking system.
This enables Claude to:
- See compiler errors and warnings after file edits
- Proactively fix issues flagged by language servers
- Provide feedback on type errors, unused variables, and other static analysis findings
Re-initialization
The reinitializeLspServerManager() function supports hot-reloading when plugins change:
- Shuts down any running servers on the old instance (fire-and-forget)
- Clears the singleton state
- Calls
initializeLspServerManager()to create a fresh instance
This is triggered by refreshActivePlugins() to pick up newly-installed plugin LSP servers without restarting the CLI.
In --bare mode (non-interactive scripted usage), LSP initialization is skipped entirely since there is no editor context to provide diagnostics for.
Checking Connection Status
The isLspConnected() function checks whether at least one language server is connected and healthy. This backs LSPTool.isEnabled(), ensuring the LSP tool only appears in the tool list when a server is actually available:
export function isLspConnected(): boolean {
const manager = getLspServerManager()
if (!manager) return false
for (const server of manager.getAllServers().values()) {
if (server.state !== 'error') return true
}
return false
}