source dump of claude code
at main 289 lines 10 kB view raw
1import { logForDebugging } from '../../utils/debug.js' 2import { isBareMode } from '../../utils/envUtils.js' 3import { errorMessage } from '../../utils/errors.js' 4import { logError } from '../../utils/log.js' 5import { 6 createLSPServerManager, 7 type LSPServerManager, 8} from './LSPServerManager.js' 9import { registerLSPNotificationHandlers } from './passiveFeedback.js' 10 11/** 12 * Initialization state of the LSP server manager 13 */ 14type InitializationState = 'not-started' | 'pending' | 'success' | 'failed' 15 16/** 17 * Global singleton instance of the LSP server manager. 18 * Initialized during Claude Code startup. 19 */ 20let lspManagerInstance: LSPServerManager | undefined 21 22/** 23 * Current initialization state 24 */ 25let initializationState: InitializationState = 'not-started' 26 27/** 28 * Error from last initialization attempt, if any 29 */ 30let initializationError: Error | undefined 31 32/** 33 * Generation counter to prevent stale initialization promises from updating state 34 */ 35let initializationGeneration = 0 36 37/** 38 * Promise that resolves when initialization completes (success or failure) 39 */ 40let initializationPromise: Promise<void> | undefined 41 42/** 43 * Test-only sync reset. shutdownLspServerManager() is async and tears down 44 * real connections; this only clears the module-scope singleton state so 45 * reinitializeLspServerManager() early-returns on 'not-started' in downstream 46 * tests on the same shard. 47 */ 48export function _resetLspManagerForTesting(): void { 49 initializationState = 'not-started' 50 initializationError = undefined 51 initializationPromise = undefined 52 initializationGeneration++ 53} 54 55/** 56 * Get the singleton LSP server manager instance. 57 * Returns undefined if not yet initialized, initialization failed, or still pending. 58 * 59 * Callers should check for undefined and handle gracefully, as initialization happens 60 * asynchronously during Claude Code startup. Use getInitializationStatus() to 61 * distinguish between pending, failed, and not-started states. 62 */ 63export function getLspServerManager(): LSPServerManager | undefined { 64 // Don't return a broken instance if initialization failed 65 if (initializationState === 'failed') { 66 return undefined 67 } 68 return lspManagerInstance 69} 70 71/** 72 * Get the current initialization status of the LSP server manager. 73 * 74 * @returns Status object with current state and error (if failed) 75 */ 76export function getInitializationStatus(): 77 | { status: 'not-started' } 78 | { status: 'pending' } 79 | { status: 'success' } 80 | { status: 'failed'; error: Error } { 81 if (initializationState === 'failed') { 82 return { 83 status: 'failed', 84 error: initializationError || new Error('Initialization failed'), 85 } 86 } 87 if (initializationState === 'not-started') { 88 return { status: 'not-started' } 89 } 90 if (initializationState === 'pending') { 91 return { status: 'pending' } 92 } 93 return { status: 'success' } 94} 95 96/** 97 * Check whether at least one language server is connected and healthy. 98 * Backs LSPTool.isEnabled(). 99 */ 100export function isLspConnected(): boolean { 101 if (initializationState === 'failed') return false 102 const manager = getLspServerManager() 103 if (!manager) return false 104 const servers = manager.getAllServers() 105 if (servers.size === 0) return false 106 for (const server of servers.values()) { 107 if (server.state !== 'error') return true 108 } 109 return false 110} 111 112/** 113 * Wait for LSP server manager initialization to complete. 114 * 115 * Returns immediately if initialization has already completed (success or failure). 116 * If initialization is pending, waits for it to complete. 117 * If initialization hasn't started, returns immediately. 118 * 119 * @returns Promise that resolves when initialization is complete 120 */ 121export async function waitForInitialization(): Promise<void> { 122 // If already initialized or failed, return immediately 123 if (initializationState === 'success' || initializationState === 'failed') { 124 return 125 } 126 127 // If pending and we have a promise, wait for it 128 if (initializationState === 'pending' && initializationPromise) { 129 await initializationPromise 130 } 131 132 // If not started, return immediately (nothing to wait for) 133} 134 135/** 136 * Initialize the LSP server manager singleton. 137 * 138 * This function is called during Claude Code startup. It synchronously creates 139 * the manager instance, then starts async initialization (loading LSP configs) 140 * in the background without blocking the startup process. 141 * 142 * Safe to call multiple times - will only initialize once (idempotent). 143 * However, if initialization previously failed, calling again will retry. 144 */ 145export function initializeLspServerManager(): void { 146 // --bare / SIMPLE: no LSP. LSP is for editor integration (diagnostics, 147 // hover, go-to-def in the REPL). Scripted -p calls have no use for it. 148 if (isBareMode()) { 149 return 150 } 151 logForDebugging('[LSP MANAGER] initializeLspServerManager() called') 152 153 // Skip if already initialized or currently initializing 154 if (lspManagerInstance !== undefined && initializationState !== 'failed') { 155 logForDebugging( 156 '[LSP MANAGER] Already initialized or initializing, skipping', 157 ) 158 return 159 } 160 161 // Reset state for retry if previous initialization failed 162 if (initializationState === 'failed') { 163 lspManagerInstance = undefined 164 initializationError = undefined 165 } 166 167 // Create the manager instance and mark as pending 168 lspManagerInstance = createLSPServerManager() 169 initializationState = 'pending' 170 logForDebugging('[LSP MANAGER] Created manager instance, state=pending') 171 172 // Increment generation to invalidate any pending initializations 173 const currentGeneration = ++initializationGeneration 174 logForDebugging( 175 `[LSP MANAGER] Starting async initialization (generation ${currentGeneration})`, 176 ) 177 178 // Start initialization asynchronously without blocking 179 // Store the promise so callers can await it via waitForInitialization() 180 initializationPromise = lspManagerInstance 181 .initialize() 182 .then(() => { 183 // Only update state if this is still the current initialization 184 if (currentGeneration === initializationGeneration) { 185 initializationState = 'success' 186 logForDebugging('LSP server manager initialized successfully') 187 188 // Register passive notification handlers for diagnostics 189 if (lspManagerInstance) { 190 registerLSPNotificationHandlers(lspManagerInstance) 191 } 192 } 193 }) 194 .catch((error: unknown) => { 195 // Only update state if this is still the current initialization 196 if (currentGeneration === initializationGeneration) { 197 initializationState = 'failed' 198 initializationError = error as Error 199 // Clear the instance since it's not usable 200 lspManagerInstance = undefined 201 202 logError(error as Error) 203 logForDebugging( 204 `Failed to initialize LSP server manager: ${errorMessage(error)}`, 205 ) 206 } 207 }) 208} 209 210/** 211 * Force re-initialization of the LSP server manager, even after a prior 212 * successful init. Called from refreshActivePlugins() after plugin caches 213 * are cleared, so newly-loaded plugin LSP servers are picked up. 214 * 215 * Fixes https://github.com/anthropics/claude-code/issues/15521: 216 * loadAllPlugins() is memoized and can be called very early in startup 217 * (via getCommands prefetch in setup.ts) before marketplaces are reconciled, 218 * caching an empty plugin list. initializeLspServerManager() then reads that 219 * stale memoized result and initializes with 0 servers. Unlike commands/agents/ 220 * hooks/MCP, LSP was never re-initialized on plugin refresh. 221 * 222 * Safe to call when no LSP plugins changed: initialize() is just config 223 * parsing (servers are lazy-started on first use). Also safe during pending 224 * init: the generation counter invalidates the in-flight promise. 225 */ 226export function reinitializeLspServerManager(): void { 227 if (initializationState === 'not-started') { 228 // initializeLspServerManager() was never called (e.g. headless subcommand 229 // path). Don't start it now. 230 return 231 } 232 233 logForDebugging('[LSP MANAGER] reinitializeLspServerManager() called') 234 235 // Best-effort shutdown of any running servers on the old instance so 236 // /reload-plugins doesn't leak child processes. Fire-and-forget: the 237 // primary use case (issue #15521) has 0 servers so this is usually a no-op. 238 if (lspManagerInstance) { 239 void lspManagerInstance.shutdown().catch(err => { 240 logForDebugging( 241 `[LSP MANAGER] old instance shutdown during reinit failed: ${errorMessage(err)}`, 242 ) 243 }) 244 } 245 246 // Force the idempotence check in initializeLspServerManager() to fall 247 // through. Generation counter handles invalidating any in-flight init. 248 lspManagerInstance = undefined 249 initializationState = 'not-started' 250 initializationError = undefined 251 252 initializeLspServerManager() 253} 254 255/** 256 * Shutdown the LSP server manager and clean up resources. 257 * 258 * This should be called during Claude Code shutdown. Stops all running LSP servers 259 * and clears internal state. Safe to call when not initialized (no-op). 260 * 261 * NOTE: Errors during shutdown are logged for monitoring but NOT propagated to the caller. 262 * State is always cleared even if shutdown fails, to prevent resource accumulation. 263 * This is acceptable during application exit when recovery is not possible. 264 * 265 * @returns Promise that resolves when shutdown completes (errors are swallowed) 266 */ 267export async function shutdownLspServerManager(): Promise<void> { 268 if (lspManagerInstance === undefined) { 269 return 270 } 271 272 try { 273 await lspManagerInstance.shutdown() 274 logForDebugging('LSP server manager shut down successfully') 275 } catch (error: unknown) { 276 logError(error as Error) 277 logForDebugging( 278 `Failed to shutdown LSP server manager: ${errorMessage(error)}`, 279 ) 280 } finally { 281 // Always clear state even if shutdown failed 282 lspManagerInstance = undefined 283 initializationState = 'not-started' 284 initializationError = undefined 285 initializationPromise = undefined 286 // Increment generation to invalidate any pending initializations 287 initializationGeneration++ 288 } 289}