1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
/**
* Diagnostics — merge push (textDocument/publishDiagnostics) + pull
* (textDocument/diagnostic) per file, dedupe, and format.
*/
export interface Diagnostic {
readonly range: {
readonly start: { readonly line: number; readonly character: number };
readonly end: { readonly line: number; readonly character: number };
};
readonly severity?: number;
readonly source?: string;
readonly message: string;
readonly code?: string | number;
}
export interface PublishDiagnosticsParams {
readonly uri: string;
readonly diagnostics: readonly Diagnostic[];
}
export interface DocumentDiagnosticReport {
readonly kind: "full" | "unchanged";
readonly items?: readonly Diagnostic[];
}
const severityNames: Record<number, string> = {
1: "ERROR",
2: "WARNING",
3: "INFO",
4: "HINT",
};
export class DiagnosticsStore {
private pushDiagnostics = new Map<string, readonly Diagnostic[]>();
private pullDiagnostics = new Map<string, readonly Diagnostic[]>();
private pushReceived = new Set<string>();
setPushDiagnostics(params: PublishDiagnosticsParams): void {
this.pushDiagnostics.set(params.uri, params.diagnostics);
this.pushReceived.add(params.uri);
}
setPullDiagnostics(uri: string, report: DocumentDiagnosticReport): void {
if (report.kind === "full" && report.items) {
this.pullDiagnostics.set(uri, report.items);
}
}
/** True if the server has pushed at least one publishDiagnostics for this URI. */
hasReceivedPush(uri: string): boolean {
return this.pushReceived.has(uri);
}
/** Clear the "received" flag so the next waitForDiagnostics poll detects fresh pushes. */
clearReceived(uri: string): void {
this.pushReceived.delete(uri);
}
getMerged(uri: string): readonly Diagnostic[] {
const push = this.pushDiagnostics.get(uri) ?? [];
const pull = this.pullDiagnostics.get(uri) ?? [];
return dedupeDiagnostics([...push, ...pull]);
}
/**
* Format diagnostics for a URI, optionally filtering by minimum severity.
* `minSeverity` includes only diagnostics with severity ≤ the given value
* (1=Error, 2=Warning, 3=Info, 4=Hint). Omit to include all.
*/
formatFiltered(uri: string, minSeverity?: number): string {
let diags = this.getMerged(uri);
if (minSeverity !== undefined) {
diags = diags.filter((d) => (d.severity ?? 0) <= minSeverity);
}
if (diags.length === 0) return "";
const lines: string[] = [];
for (const d of diags) {
const sev = d.severity ? (severityNames[d.severity] ?? "UNKNOWN") : "UNKNOWN";
const line = d.range.start.line + 1;
const col = d.range.start.character + 1;
const src = d.source ? ` [${d.source}]` : "";
const code = d.code ? ` (${d.code})` : "";
lines.push(`${sev}${code}${src} L${line}:${col}: ${d.message}`);
}
return lines.join("\n");
}
format(uri: string): string {
return this.formatFiltered(uri);
}
}
export function diagnosticKey(d: Diagnostic): string {
const r = d.range;
return `${r.start.line}:${r.start.character}-${r.end.line}:${r.end.character}:${d.severity ?? 0}:${d.message}`;
}
function dedupeDiagnostics(diags: readonly Diagnostic[]): readonly Diagnostic[] {
const seen = new Set<string>();
const result: Diagnostic[] = [];
for (const d of diags) {
const key = diagnosticKey(d);
if (!seen.has(key)) {
seen.add(key);
result.push(d);
}
}
return result;
}
|