import { fileURLToPath } from 'url' import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import { logForDebugging } from '../../utils/debug.js' import { toError } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { jsonStringify } from '../../utils/slowOperations.js' import type { DiagnosticFile } from '../diagnosticTracking.js' import { registerPendingLSPDiagnostic } from './LSPDiagnosticRegistry.js' import type { LSPServerManager } from './LSPServerManager.js' /** * Map LSP severity to Claude diagnostic severity * * Maps LSP severity numbers to Claude diagnostic severity strings. * Accepts numeric severity values (1=Error, 2=Warning, 3=Information, 4=Hint) * or undefined, defaulting to 'Error' for invalid/missing values. */ function mapLSPSeverity( lspSeverity: number | undefined, ): 'Error' | 'Warning' | 'Info' | 'Hint' { // LSP DiagnosticSeverity enum: // 1 = Error, 2 = Warning, 3 = Information, 4 = Hint switch (lspSeverity) { case 1: return 'Error' case 2: return 'Warning' case 3: return 'Info' case 4: return 'Hint' default: return 'Error' } } /** * Convert LSP diagnostics to Claude diagnostic format * * Converts LSP PublishDiagnosticsParams to DiagnosticFile[] format * used by Claude's attachment system. */ export function formatDiagnosticsForAttachment( params: PublishDiagnosticsParams, ): DiagnosticFile[] { // Parse URI (may be file:// or plain path) and normalize to file system path let uri: string try { // Handle both file:// URIs and plain paths uri = params.uri.startsWith('file://') ? fileURLToPath(params.uri) : params.uri } catch (error) { const err = toError(error) logError(err) logForDebugging( `Failed to convert URI to file path: ${params.uri}. Error: ${err.message}. Using original URI as fallback.`, ) // Gracefully fallback to original URI - LSP servers may send malformed URIs uri = params.uri } const diagnostics = params.diagnostics.map( (diag: { message: string severity?: number range: { start: { line: number; character: number } end: { line: number; character: number } } source?: string code?: string | number }) => ({ message: diag.message, severity: mapLSPSeverity(diag.severity), range: { start: { line: diag.range.start.line, character: diag.range.start.character, }, end: { line: diag.range.end.line, character: diag.range.end.character, }, }, source: diag.source, code: diag.code !== undefined && diag.code !== null ? String(diag.code) : undefined, }), ) return [ { uri, diagnostics, }, ] } /** * Handler registration result with tracking data */ export type HandlerRegistrationResult = { /** Total number of servers */ totalServers: number /** Number of successful registrations */ successCount: number /** Registration errors per server */ registrationErrors: Array<{ serverName: string; error: string }> /** Runtime failure tracking (shared across all handler invocations) */ diagnosticFailures: Map } /** * Register LSP notification handlers on all servers * * Sets up handlers to listen for textDocument/publishDiagnostics notifications * from all LSP servers and routes them to Claude's diagnostic system. * Uses public getAllServers() API for clean access to server instances. * * @returns Tracking data for registration status and runtime failures */ export function registerLSPNotificationHandlers( manager: LSPServerManager, ): HandlerRegistrationResult { // Register handlers on all configured servers to capture diagnostics from any language const servers = manager.getAllServers() // Track partial failures - allow successful server registrations even if some fail const registrationErrors: Array<{ serverName: string; error: string }> = [] let successCount = 0 // Track consecutive failures per server to warn users after 3+ failures const diagnosticFailures: Map = new Map() for (const [serverName, serverInstance] of servers.entries()) { try { // Validate server instance has onNotification method if ( !serverInstance || typeof serverInstance.onNotification !== 'function' ) { const errorMsg = !serverInstance ? 'Server instance is null/undefined' : 'Server instance has no onNotification method' registrationErrors.push({ serverName, error: errorMsg }) const err = new Error(`${errorMsg} for ${serverName}`) logError(err) logForDebugging( `Skipping handler registration for ${serverName}: ${errorMsg}`, ) continue // Skip this server but track the failure } // Errors are isolated to avoid breaking other servers serverInstance.onNotification( 'textDocument/publishDiagnostics', (params: unknown) => { logForDebugging( `[PASSIVE DIAGNOSTICS] Handler invoked for ${serverName}! Params type: ${typeof params}`, ) try { // Validate params structure before casting if ( !params || typeof params !== 'object' || !('uri' in params) || !('diagnostics' in params) ) { const err = new Error( `LSP server ${serverName} sent invalid diagnostic params (missing uri or diagnostics)`, ) logError(err) logForDebugging( `Invalid diagnostic params from ${serverName}: ${jsonStringify(params)}`, ) return } const diagnosticParams = params as PublishDiagnosticsParams logForDebugging( `Received diagnostics from ${serverName}: ${diagnosticParams.diagnostics.length} diagnostic(s) for ${diagnosticParams.uri}`, ) // Convert LSP diagnostics to Claude format (can throw on invalid URIs) const diagnosticFiles = formatDiagnosticsForAttachment(diagnosticParams) // Only send notification if there are diagnostics const firstFile = diagnosticFiles[0] if ( !firstFile || diagnosticFiles.length === 0 || firstFile.diagnostics.length === 0 ) { logForDebugging( `Skipping empty diagnostics from ${serverName} for ${diagnosticParams.uri}`, ) return } // Register diagnostics for async delivery via attachment system // Follows same pattern as AsyncHookRegistry for consistent async attachment delivery try { registerPendingLSPDiagnostic({ serverName, files: diagnosticFiles, }) logForDebugging( `LSP Diagnostics: Registered ${diagnosticFiles.length} diagnostic file(s) from ${serverName} for async delivery`, ) // Success - reset failure counter for this server diagnosticFailures.delete(serverName) } catch (error) { const err = toError(error) logError(err) logForDebugging( `Error registering LSP diagnostics from ${serverName}: ` + `URI: ${diagnosticParams.uri}, ` + `Diagnostic count: ${firstFile.diagnostics.length}, ` + `Error: ${err.message}`, ) // Track consecutive failures and warn after 3+ const failures = diagnosticFailures.get(serverName) || { count: 0, lastError: '', } failures.count++ failures.lastError = err.message diagnosticFailures.set(serverName, failures) if (failures.count >= 3) { logForDebugging( `WARNING: LSP diagnostic handler for ${serverName} has failed ${failures.count} times consecutively. ` + `Last error: ${failures.lastError}. ` + `This may indicate a problem with the LSP server or diagnostic processing. ` + `Check logs for details.`, ) } } } catch (error) { // Catch any unexpected errors from the entire handler to prevent breaking the notification loop const err = toError(error) logError(err) logForDebugging( `Unexpected error processing diagnostics from ${serverName}: ${err.message}`, ) // Track consecutive failures and warn after 3+ const failures = diagnosticFailures.get(serverName) || { count: 0, lastError: '', } failures.count++ failures.lastError = err.message diagnosticFailures.set(serverName, failures) if (failures.count >= 3) { logForDebugging( `WARNING: LSP diagnostic handler for ${serverName} has failed ${failures.count} times consecutively. ` + `Last error: ${failures.lastError}. ` + `This may indicate a problem with the LSP server or diagnostic processing. ` + `Check logs for details.`, ) } // Don't re-throw - isolate errors to this server only } }, ) logForDebugging(`Registered diagnostics handler for ${serverName}`) successCount++ } catch (error) { const err = toError(error) registrationErrors.push({ serverName, error: err.message, }) logError(err) logForDebugging( `Failed to register diagnostics handler for ${serverName}: ` + `Error: ${err.message}`, ) } } // Report overall registration status const totalServers = servers.size if (registrationErrors.length > 0) { const failedServers = registrationErrors .map(e => `${e.serverName} (${e.error})`) .join(', ') // Log aggregate failures for tracking logError( new Error( `Failed to register diagnostics for ${registrationErrors.length} LSP server(s): ${failedServers}`, ), ) logForDebugging( `LSP notification handler registration: ${successCount}/${totalServers} succeeded. ` + `Failed servers: ${failedServers}. ` + `Diagnostics from failed servers will not be delivered.`, ) } else { logForDebugging( `LSP notification handlers registered successfully for all ${totalServers} server(s)`, ) } // Return tracking data for monitoring and testing return { totalServers, successCount, registrationErrors, diagnosticFailures, } }