AI Assistant

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:

  1. LSPServerManager (factory function): Routes requests to the correct language server based on file extension
  2. manager.ts (singleton): Manages the global lifecycle of the LSPServerManager instance

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
}
initialize()() => Promise<void>

Loads all configured LSP servers from plugins, builds the extension-to-server mapping, and creates server instances.

getServerForFile()(filePath: string) => LSPServerInstance | undefined

Returns the appropriate server instance for a given file path based on its extension.

ensureServerStarted()(filePath: string) => Promise<LSPServerInstance | undefined>

Lazily starts the language server for a file if not already running.

sendRequest()(filePath, method, params) => Promise<T | undefined>

Sends an LSP request (e.g., textDocument/definition) to the appropriate server.

openFile() / changeFile() / saveFile() / closeFile()(filePath, content?) => Promise<void>

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:

LanguageTypical ExtensionsServer
TypeScript / JavaScript.ts, .tsx, .js, .jsxTypeScript Language Server
Python.pyPyright / Pylsp
Go.gogopls
Rust.rsrust-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:

  1. Extracts the file extension from the path
  2. Looks up the extension in the extensionMap to find the server name
  3. Retrieves the LSPServerInstance from the servers map
  4. Lazily starts the server if it has not been initialized yet
const instance = manager.getServerForFile('/project/src/index.ts')
// Returns the TypeScript LSP server instance

Initialization States

The singleton manager in manager.ts tracks initialization through a state machine:

not-started --> pending --> success
                       \--> failed
not-startedInitializationState

Manager has not been created. Returned when initializeLspServerManager() has never been called (e.g., headless subcommands).

pendingInitializationState

Manager created, async initialization in progress. The initializationPromise can be awaited.

successInitializationState

Initialization completed successfully. Passive notification handlers are registered.

failedInitializationState

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:

  1. Shuts down any running servers on the old instance (fire-and-forget)
  2. Clears the singleton state
  3. 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
}