|
10 | 10 | */ |
11 | 11 |
|
12 | 12 | import { createRequire } from 'node:module'; |
13 | | -import { pathToFileURL } from 'node:url'; |
| 13 | +import { pathToFileURL, fileURLToPath } from 'node:url'; |
14 | 14 |
|
15 | 15 | import type { ProcessHandle, SandboxProcessManager } from '../sandbox/process-manager'; |
16 | 16 | import type { LSPServerDef } from './types'; |
@@ -101,6 +101,31 @@ function toFileUri(fsPath: string): string { |
101 | 101 | return pathToFileURL(fsPath).toString(); |
102 | 102 | } |
103 | 103 |
|
| 104 | +/** |
| 105 | + * Normalize a file:// URI to a canonical fs-path-based key for diagnostics |
| 106 | + * map storage/lookup. On Windows, different LSP servers emit different |
| 107 | + * canonical forms for the same path (e.g. `file:///C:/...` vs |
| 108 | + * `file:///c%3A/...`), so we convert back to an OS path and compare those |
| 109 | + * instead of comparing URI strings directly. |
| 110 | + */ |
| 111 | +export function diagnosticsKey(uriOrPath: string): string { |
| 112 | + let fsPath: string; |
| 113 | + try { |
| 114 | + fsPath = uriOrPath.startsWith('file:') ? fileURLToPath(uriOrPath) : uriOrPath; |
| 115 | + } catch { |
| 116 | + return uriOrPath; |
| 117 | + } |
| 118 | + // Normalize Windows drive-letter paths so they compare equal regardless of |
| 119 | + // drive-letter casing or whether fileURLToPath produced a leading slash |
| 120 | + // (e.g. '/C:/Users/...' vs 'C:/Users/...' vs 'c:\\Users\\...'), independent |
| 121 | + // of the OS this code happens to run on. |
| 122 | + const driveMatch = fsPath.match(/^[\\/]?([a-zA-Z]):([\\/].*)$/); |
| 123 | + if (driveMatch) { |
| 124 | + return `${driveMatch[1]!.toLowerCase()}:${driveMatch[2]}`; |
| 125 | + } |
| 126 | + return fsPath; |
| 127 | +} |
| 128 | + |
104 | 129 | // ============================================================================= |
105 | 130 | // Timeout Helper |
106 | 131 | // ============================================================================= |
@@ -175,7 +200,7 @@ export class LSPClient { |
175 | 200 |
|
176 | 201 | // Listen for published diagnostics |
177 | 202 | this.connection.onNotification('textDocument/publishDiagnostics', (params: any) => { |
178 | | - this.diagnostics.set(params.uri, params.diagnostics); |
| 203 | + this.diagnostics.set(diagnosticsKey(params.uri), params.diagnostics); |
179 | 204 | }); |
180 | 205 |
|
181 | 206 | this.connection.listen(); |
@@ -280,7 +305,7 @@ export class LSPClient { |
280 | 305 | notifyOpen(filePath: string, content: string, languageId: string): void { |
281 | 306 | if (!this.connection) return; |
282 | 307 | const uri = toFileUri(filePath); |
283 | | - this.diagnostics.delete(uri); |
| 308 | + this.diagnostics.delete(diagnosticsKey(uri)); |
284 | 309 | this.connection.sendNotification('textDocument/didOpen', { |
285 | 310 | textDocument: { uri, languageId, version: 0, text: content }, |
286 | 311 | }); |
@@ -314,7 +339,7 @@ export class LSPClient { |
314 | 339 | settleMs: number = 500, |
315 | 340 | ): Promise<any[]> { |
316 | 341 | if (!this.connection) return []; |
317 | | - const uri = toFileUri(filePath); |
| 342 | + const uri = diagnosticsKey(toFileUri(filePath)); |
318 | 343 | const startTime = Date.now(); |
319 | 344 | const initialDiagnostics = this.diagnostics.get(uri); |
320 | 345 | let emptyReceivedAt: number | undefined; |
@@ -350,7 +375,7 @@ export class LSPClient { |
350 | 375 | notifyClose(filePath: string): void { |
351 | 376 | if (!this.connection) return; |
352 | 377 | const uri = toFileUri(filePath); |
353 | | - this.diagnostics.delete(uri); |
| 378 | + this.diagnostics.delete(diagnosticsKey(uri)); |
354 | 379 | this.connection.sendNotification('textDocument/didClose', { |
355 | 380 | textDocument: { uri }, |
356 | 381 | }); |
|
0 commit comments