290 lines
9.8 KiB
TypeScript
290 lines
9.8 KiB
TypeScript
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<void> | 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<void> {
|
|
// 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<void> {
|
|
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++
|
|
}
|
|
}
|