source dump of claude code
at main 328 lines 11 kB view raw
1import { fileURLToPath } from 'url' 2import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol' 3import { logForDebugging } from '../../utils/debug.js' 4import { toError } from '../../utils/errors.js' 5import { logError } from '../../utils/log.js' 6import { jsonStringify } from '../../utils/slowOperations.js' 7import type { DiagnosticFile } from '../diagnosticTracking.js' 8import { registerPendingLSPDiagnostic } from './LSPDiagnosticRegistry.js' 9import type { LSPServerManager } from './LSPServerManager.js' 10 11/** 12 * Map LSP severity to Claude diagnostic severity 13 * 14 * Maps LSP severity numbers to Claude diagnostic severity strings. 15 * Accepts numeric severity values (1=Error, 2=Warning, 3=Information, 4=Hint) 16 * or undefined, defaulting to 'Error' for invalid/missing values. 17 */ 18function mapLSPSeverity( 19 lspSeverity: number | undefined, 20): 'Error' | 'Warning' | 'Info' | 'Hint' { 21 // LSP DiagnosticSeverity enum: 22 // 1 = Error, 2 = Warning, 3 = Information, 4 = Hint 23 switch (lspSeverity) { 24 case 1: 25 return 'Error' 26 case 2: 27 return 'Warning' 28 case 3: 29 return 'Info' 30 case 4: 31 return 'Hint' 32 default: 33 return 'Error' 34 } 35} 36 37/** 38 * Convert LSP diagnostics to Claude diagnostic format 39 * 40 * Converts LSP PublishDiagnosticsParams to DiagnosticFile[] format 41 * used by Claude's attachment system. 42 */ 43export function formatDiagnosticsForAttachment( 44 params: PublishDiagnosticsParams, 45): DiagnosticFile[] { 46 // Parse URI (may be file:// or plain path) and normalize to file system path 47 let uri: string 48 try { 49 // Handle both file:// URIs and plain paths 50 uri = params.uri.startsWith('file://') 51 ? fileURLToPath(params.uri) 52 : params.uri 53 } catch (error) { 54 const err = toError(error) 55 logError(err) 56 logForDebugging( 57 `Failed to convert URI to file path: ${params.uri}. Error: ${err.message}. Using original URI as fallback.`, 58 ) 59 // Gracefully fallback to original URI - LSP servers may send malformed URIs 60 uri = params.uri 61 } 62 63 const diagnostics = params.diagnostics.map( 64 (diag: { 65 message: string 66 severity?: number 67 range: { 68 start: { line: number; character: number } 69 end: { line: number; character: number } 70 } 71 source?: string 72 code?: string | number 73 }) => ({ 74 message: diag.message, 75 severity: mapLSPSeverity(diag.severity), 76 range: { 77 start: { 78 line: diag.range.start.line, 79 character: diag.range.start.character, 80 }, 81 end: { 82 line: diag.range.end.line, 83 character: diag.range.end.character, 84 }, 85 }, 86 source: diag.source, 87 code: 88 diag.code !== undefined && diag.code !== null 89 ? String(diag.code) 90 : undefined, 91 }), 92 ) 93 94 return [ 95 { 96 uri, 97 diagnostics, 98 }, 99 ] 100} 101 102/** 103 * Handler registration result with tracking data 104 */ 105export type HandlerRegistrationResult = { 106 /** Total number of servers */ 107 totalServers: number 108 /** Number of successful registrations */ 109 successCount: number 110 /** Registration errors per server */ 111 registrationErrors: Array<{ serverName: string; error: string }> 112 /** Runtime failure tracking (shared across all handler invocations) */ 113 diagnosticFailures: Map<string, { count: number; lastError: string }> 114} 115 116/** 117 * Register LSP notification handlers on all servers 118 * 119 * Sets up handlers to listen for textDocument/publishDiagnostics notifications 120 * from all LSP servers and routes them to Claude's diagnostic system. 121 * Uses public getAllServers() API for clean access to server instances. 122 * 123 * @returns Tracking data for registration status and runtime failures 124 */ 125export function registerLSPNotificationHandlers( 126 manager: LSPServerManager, 127): HandlerRegistrationResult { 128 // Register handlers on all configured servers to capture diagnostics from any language 129 const servers = manager.getAllServers() 130 131 // Track partial failures - allow successful server registrations even if some fail 132 const registrationErrors: Array<{ serverName: string; error: string }> = [] 133 let successCount = 0 134 135 // Track consecutive failures per server to warn users after 3+ failures 136 const diagnosticFailures: Map<string, { count: number; lastError: string }> = 137 new Map() 138 139 for (const [serverName, serverInstance] of servers.entries()) { 140 try { 141 // Validate server instance has onNotification method 142 if ( 143 !serverInstance || 144 typeof serverInstance.onNotification !== 'function' 145 ) { 146 const errorMsg = !serverInstance 147 ? 'Server instance is null/undefined' 148 : 'Server instance has no onNotification method' 149 150 registrationErrors.push({ serverName, error: errorMsg }) 151 152 const err = new Error(`${errorMsg} for ${serverName}`) 153 logError(err) 154 logForDebugging( 155 `Skipping handler registration for ${serverName}: ${errorMsg}`, 156 ) 157 continue // Skip this server but track the failure 158 } 159 160 // Errors are isolated to avoid breaking other servers 161 serverInstance.onNotification( 162 'textDocument/publishDiagnostics', 163 (params: unknown) => { 164 logForDebugging( 165 `[PASSIVE DIAGNOSTICS] Handler invoked for ${serverName}! Params type: ${typeof params}`, 166 ) 167 try { 168 // Validate params structure before casting 169 if ( 170 !params || 171 typeof params !== 'object' || 172 !('uri' in params) || 173 !('diagnostics' in params) 174 ) { 175 const err = new Error( 176 `LSP server ${serverName} sent invalid diagnostic params (missing uri or diagnostics)`, 177 ) 178 logError(err) 179 logForDebugging( 180 `Invalid diagnostic params from ${serverName}: ${jsonStringify(params)}`, 181 ) 182 return 183 } 184 185 const diagnosticParams = params as PublishDiagnosticsParams 186 logForDebugging( 187 `Received diagnostics from ${serverName}: ${diagnosticParams.diagnostics.length} diagnostic(s) for ${diagnosticParams.uri}`, 188 ) 189 190 // Convert LSP diagnostics to Claude format (can throw on invalid URIs) 191 const diagnosticFiles = 192 formatDiagnosticsForAttachment(diagnosticParams) 193 194 // Only send notification if there are diagnostics 195 const firstFile = diagnosticFiles[0] 196 if ( 197 !firstFile || 198 diagnosticFiles.length === 0 || 199 firstFile.diagnostics.length === 0 200 ) { 201 logForDebugging( 202 `Skipping empty diagnostics from ${serverName} for ${diagnosticParams.uri}`, 203 ) 204 return 205 } 206 207 // Register diagnostics for async delivery via attachment system 208 // Follows same pattern as AsyncHookRegistry for consistent async attachment delivery 209 try { 210 registerPendingLSPDiagnostic({ 211 serverName, 212 files: diagnosticFiles, 213 }) 214 215 logForDebugging( 216 `LSP Diagnostics: Registered ${diagnosticFiles.length} diagnostic file(s) from ${serverName} for async delivery`, 217 ) 218 219 // Success - reset failure counter for this server 220 diagnosticFailures.delete(serverName) 221 } catch (error) { 222 const err = toError(error) 223 logError(err) 224 logForDebugging( 225 `Error registering LSP diagnostics from ${serverName}: ` + 226 `URI: ${diagnosticParams.uri}, ` + 227 `Diagnostic count: ${firstFile.diagnostics.length}, ` + 228 `Error: ${err.message}`, 229 ) 230 231 // Track consecutive failures and warn after 3+ 232 const failures = diagnosticFailures.get(serverName) || { 233 count: 0, 234 lastError: '', 235 } 236 failures.count++ 237 failures.lastError = err.message 238 diagnosticFailures.set(serverName, failures) 239 240 if (failures.count >= 3) { 241 logForDebugging( 242 `WARNING: LSP diagnostic handler for ${serverName} has failed ${failures.count} times consecutively. ` + 243 `Last error: ${failures.lastError}. ` + 244 `This may indicate a problem with the LSP server or diagnostic processing. ` + 245 `Check logs for details.`, 246 ) 247 } 248 } 249 } catch (error) { 250 // Catch any unexpected errors from the entire handler to prevent breaking the notification loop 251 const err = toError(error) 252 logError(err) 253 logForDebugging( 254 `Unexpected error processing diagnostics from ${serverName}: ${err.message}`, 255 ) 256 257 // Track consecutive failures and warn after 3+ 258 const failures = diagnosticFailures.get(serverName) || { 259 count: 0, 260 lastError: '', 261 } 262 failures.count++ 263 failures.lastError = err.message 264 diagnosticFailures.set(serverName, failures) 265 266 if (failures.count >= 3) { 267 logForDebugging( 268 `WARNING: LSP diagnostic handler for ${serverName} has failed ${failures.count} times consecutively. ` + 269 `Last error: ${failures.lastError}. ` + 270 `This may indicate a problem with the LSP server or diagnostic processing. ` + 271 `Check logs for details.`, 272 ) 273 } 274 275 // Don't re-throw - isolate errors to this server only 276 } 277 }, 278 ) 279 280 logForDebugging(`Registered diagnostics handler for ${serverName}`) 281 successCount++ 282 } catch (error) { 283 const err = toError(error) 284 285 registrationErrors.push({ 286 serverName, 287 error: err.message, 288 }) 289 290 logError(err) 291 logForDebugging( 292 `Failed to register diagnostics handler for ${serverName}: ` + 293 `Error: ${err.message}`, 294 ) 295 } 296 } 297 298 // Report overall registration status 299 const totalServers = servers.size 300 if (registrationErrors.length > 0) { 301 const failedServers = registrationErrors 302 .map(e => `${e.serverName} (${e.error})`) 303 .join(', ') 304 // Log aggregate failures for tracking 305 logError( 306 new Error( 307 `Failed to register diagnostics for ${registrationErrors.length} LSP server(s): ${failedServers}`, 308 ), 309 ) 310 logForDebugging( 311 `LSP notification handler registration: ${successCount}/${totalServers} succeeded. ` + 312 `Failed servers: ${failedServers}. ` + 313 `Diagnostics from failed servers will not be delivered.`, 314 ) 315 } else { 316 logForDebugging( 317 `LSP notification handlers registered successfully for all ${totalServers} server(s)`, 318 ) 319 } 320 321 // Return tracking data for monitoring and testing 322 return { 323 totalServers, 324 successCount, 325 registrationErrors, 326 diagnosticFailures, 327 } 328}