import { logForDebugging } from '../../utils/debug.js' import { isBareMode } from '../../utils/envUtils.js' import { errorMessage } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { createLSPServerManager, type LSPServerManager, } from './LSPServerManager.js' import { registerLSPNotificationHandlers } from './passiveFeedback.js' /** * Initialization state of the LSP server manager */ type InitializationState = 'not-started' | 'pending' | 'success' | 'failed' /** * Global singleton instance of the LSP server manager. * Initialized during Claude Code startup. */ let lspManagerInstance: LSPServerManager | undefined /** * Current initialization state */ let initializationState: InitializationState = 'not-started' /** * Error from last initialization attempt, if any */ let initializationError: Error | undefined /** * Generation counter to prevent stale initialization promises from updating state */ let initializationGeneration = 0 /** * Promise that resolves when initialization completes (success or failure) */ let initializationPromise: Promise | undefined /** * Test-only sync reset. shutdownLspServerManager() is async and tears down * real connections; this only clears the module-scope singleton state so * reinitializeLspServerManager() early-returns on 'not-started' in downstream * tests on the same shard. */ export function _resetLspManagerForTesting(): void { initializationState = 'not-started' initializationError = undefined initializationPromise = undefined initializationGeneration++ } /** * Get the singleton LSP server manager instance. * Returns undefined if not yet initialized, initialization failed, or still pending. * * Callers should check for undefined and handle gracefully, as initialization happens * asynchronously during Claude Code startup. Use getInitializationStatus() to * distinguish between pending, failed, and not-started states. */ export function getLspServerManager(): LSPServerManager | undefined { // Don't return a broken instance if initialization failed if (initializationState === 'failed') { return undefined } return lspManagerInstance } /** * Get the current initialization status of the LSP server manager. * * @returns Status object with current state and error (if failed) */ export function getInitializationStatus(): | { status: 'not-started' } | { status: 'pending' } | { status: 'success' } | { status: 'failed'; error: Error } { if (initializationState === 'failed') { return { status: 'failed', error: initializationError || new Error('Initialization failed'), } } if (initializationState === 'not-started') { return { status: 'not-started' } } if (initializationState === 'pending') { return { status: 'pending' } } return { status: 'success' } } /** * Check whether at least one language server is connected and healthy. * Backs LSPTool.isEnabled(). */ export function isLspConnected(): boolean { if (initializationState === 'failed') return false const manager = getLspServerManager() if (!manager) return false const servers = manager.getAllServers() if (servers.size === 0) return false for (const server of servers.values()) { if (server.state !== 'error') return true } return false } /** * Wait for LSP server manager initialization to complete. * * Returns immediately if initialization has already completed (success or failure). * If initialization is pending, waits for it to complete. * If initialization hasn't started, returns immediately. * * @returns Promise that resolves when initialization is complete */ export async function waitForInitialization(): Promise { // If already initialized or failed, return immediately if (initializationState === 'success' || initializationState === 'failed') { return } // If pending and we have a promise, wait for it if (initializationState === 'pending' && initializationPromise) { await initializationPromise } // If not started, return immediately (nothing to wait for) } /** * Initialize the LSP server manager singleton. * * This function is called during Claude Code startup. It synchronously creates * the manager instance, then starts async initialization (loading LSP configs) * in the background without blocking the startup process. * * Safe to call multiple times - will only initialize once (idempotent). * However, if initialization previously failed, calling again will retry. */ export function initializeLspServerManager(): void { // --bare / SIMPLE: no LSP. LSP is for editor integration (diagnostics, // hover, go-to-def in the REPL). Scripted -p calls have no use for it. if (isBareMode()) { return } logForDebugging('[LSP MANAGER] initializeLspServerManager() called') // Skip if already initialized or currently initializing if (lspManagerInstance !== undefined && initializationState !== 'failed') { logForDebugging( '[LSP MANAGER] Already initialized or initializing, skipping', ) return } // Reset state for retry if previous initialization failed if (initializationState === 'failed') { lspManagerInstance = undefined initializationError = undefined } // Create the manager instance and mark as pending lspManagerInstance = createLSPServerManager() initializationState = 'pending' logForDebugging('[LSP MANAGER] Created manager instance, state=pending') // Increment generation to invalidate any pending initializations const currentGeneration = ++initializationGeneration logForDebugging( `[LSP MANAGER] Starting async initialization (generation ${currentGeneration})`, ) // Start initialization asynchronously without blocking // Store the promise so callers can await it via waitForInitialization() initializationPromise = lspManagerInstance .initialize() .then(() => { // Only update state if this is still the current initialization if (currentGeneration === initializationGeneration) { initializationState = 'success' logForDebugging('LSP server manager initialized successfully') // Register passive notification handlers for diagnostics if (lspManagerInstance) { registerLSPNotificationHandlers(lspManagerInstance) } } }) .catch((error: unknown) => { // Only update state if this is still the current initialization if (currentGeneration === initializationGeneration) { initializationState = 'failed' initializationError = error as Error // Clear the instance since it's not usable lspManagerInstance = undefined logError(error as Error) logForDebugging( `Failed to initialize LSP server manager: ${errorMessage(error)}`, ) } }) } /** * Force re-initialization of the LSP server manager, even after a prior * successful init. Called from refreshActivePlugins() after plugin caches * are cleared, so newly-loaded plugin LSP servers are picked up. * * Fixes https://github.com/anthropics/claude-code/issues/15521: * loadAllPlugins() is memoized and can be called very early in startup * (via getCommands prefetch in setup.ts) before marketplaces are reconciled, * caching an empty plugin list. initializeLspServerManager() then reads that * stale memoized result and initializes with 0 servers. Unlike commands/agents/ * hooks/MCP, LSP was never re-initialized on plugin refresh. * * Safe to call when no LSP plugins changed: initialize() is just config * parsing (servers are lazy-started on first use). Also safe during pending * init: the generation counter invalidates the in-flight promise. */ export function reinitializeLspServerManager(): void { if (initializationState === 'not-started') { // initializeLspServerManager() was never called (e.g. headless subcommand // path). Don't start it now. return } logForDebugging('[LSP MANAGER] reinitializeLspServerManager() called') // Best-effort shutdown of any running servers on the old instance so // /reload-plugins doesn't leak child processes. Fire-and-forget: the // primary use case (issue #15521) has 0 servers so this is usually a no-op. if (lspManagerInstance) { void lspManagerInstance.shutdown().catch(err => { logForDebugging( `[LSP MANAGER] old instance shutdown during reinit failed: ${errorMessage(err)}`, ) }) } // Force the idempotence check in initializeLspServerManager() to fall // through. Generation counter handles invalidating any in-flight init. lspManagerInstance = undefined initializationState = 'not-started' initializationError = undefined initializeLspServerManager() } /** * Shutdown the LSP server manager and clean up resources. * * This should be called during Claude Code shutdown. Stops all running LSP servers * and clears internal state. Safe to call when not initialized (no-op). * * NOTE: Errors during shutdown are logged for monitoring but NOT propagated to the caller. * State is always cleared even if shutdown fails, to prevent resource accumulation. * This is acceptable during application exit when recovery is not possible. * * @returns Promise that resolves when shutdown completes (errors are swallowed) */ export async function shutdownLspServerManager(): Promise { if (lspManagerInstance === undefined) { return } try { await lspManagerInstance.shutdown() logForDebugging('LSP server manager shut down successfully') } catch (error: unknown) { logError(error as Error) logForDebugging( `Failed to shutdown LSP server manager: ${errorMessage(error)}`, ) } finally { // Always clear state even if shutdown failed lspManagerInstance = undefined initializationState = 'not-started' initializationError = undefined initializationPromise = undefined // Increment generation to invalidate any pending initializations initializationGeneration++ } }