Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostDiagnostics.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
/* eslint-disable local/code-no-native-private */
7
8
import { localize } from '../../../nls.js';
9
import { IMarkerData, MarkerSeverity } from '../../../platform/markers/common/markers.js';
10
import { URI, UriComponents } from '../../../base/common/uri.js';
11
import type * as vscode from 'vscode';
12
import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from './extHost.protocol.js';
13
import { DiagnosticSeverity } from './extHostTypes.js';
14
import * as converter from './extHostTypeConverters.js';
15
import { Event, Emitter, DebounceEmitter } from '../../../base/common/event.js';
16
import { ILogService } from '../../../platform/log/common/log.js';
17
import { ResourceMap } from '../../../base/common/map.js';
18
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
19
import { IExtHostFileSystemInfo } from './extHostFileSystemInfo.js';
20
import { IExtUri } from '../../../base/common/resources.js';
21
import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';
22
23
export class DiagnosticCollection implements vscode.DiagnosticCollection {
24
25
readonly #proxy: MainThreadDiagnosticsShape | undefined;
26
readonly #onDidChangeDiagnostics: Emitter<readonly vscode.Uri[]>;
27
readonly #data: ResourceMap<vscode.Diagnostic[]>;
28
29
private _isDisposed = false;
30
31
constructor(
32
private readonly _name: string,
33
private readonly _owner: string,
34
private readonly _maxDiagnosticsTotal: number,
35
private readonly _maxDiagnosticsPerFile: number,
36
private readonly _modelVersionIdProvider: (uri: URI) => number | undefined,
37
extUri: IExtUri,
38
proxy: MainThreadDiagnosticsShape | undefined,
39
onDidChangeDiagnostics: Emitter<readonly vscode.Uri[]>
40
) {
41
this._maxDiagnosticsTotal = Math.max(_maxDiagnosticsPerFile, _maxDiagnosticsTotal);
42
this.#data = new ResourceMap(uri => extUri.getComparisonKey(uri));
43
this.#proxy = proxy;
44
this.#onDidChangeDiagnostics = onDidChangeDiagnostics;
45
}
46
47
dispose(): void {
48
if (!this._isDisposed) {
49
this.#onDidChangeDiagnostics.fire([...this.#data.keys()]);
50
this.#proxy?.$clear(this._owner);
51
this.#data.clear();
52
this._isDisposed = true;
53
}
54
}
55
56
get name(): string {
57
this._checkDisposed();
58
return this._name;
59
}
60
61
set(uri: vscode.Uri, diagnostics: ReadonlyArray<vscode.Diagnostic>): void;
62
set(entries: ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>): void;
63
set(first: vscode.Uri | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>, diagnostics?: ReadonlyArray<vscode.Diagnostic>) {
64
65
if (!first) {
66
// this set-call is a clear-call
67
this.clear();
68
return;
69
}
70
71
// the actual implementation for #set
72
73
this._checkDisposed();
74
let toSync: vscode.Uri[] = [];
75
76
if (URI.isUri(first)) {
77
78
if (!diagnostics) {
79
// remove this entry
80
this.delete(first);
81
return;
82
}
83
84
// update single row
85
this.#data.set(first, diagnostics.slice());
86
toSync = [first];
87
88
} else if (Array.isArray(first)) {
89
// update many rows
90
toSync = [];
91
let lastUri: vscode.Uri | undefined;
92
93
// ensure stable-sort
94
first = [...first].sort(DiagnosticCollection._compareIndexedTuplesByUri);
95
96
for (const tuple of first) {
97
const [uri, diagnostics] = tuple;
98
if (!lastUri || uri.toString() !== lastUri.toString()) {
99
if (lastUri && this.#data.get(lastUri)!.length === 0) {
100
this.#data.delete(lastUri);
101
}
102
lastUri = uri;
103
toSync.push(uri);
104
this.#data.set(uri, []);
105
}
106
107
if (!diagnostics) {
108
// [Uri, undefined] means clear this
109
const currentDiagnostics = this.#data.get(uri);
110
if (currentDiagnostics) {
111
currentDiagnostics.length = 0;
112
}
113
} else {
114
const currentDiagnostics = this.#data.get(uri);
115
currentDiagnostics?.push(...diagnostics);
116
}
117
}
118
}
119
120
// send event for extensions
121
this.#onDidChangeDiagnostics.fire(toSync);
122
123
// compute change and send to main side
124
if (!this.#proxy) {
125
return;
126
}
127
const entries: [URI, IMarkerData[]][] = [];
128
let totalMarkerCount = 0;
129
for (const uri of toSync) {
130
let marker: IMarkerData[] = [];
131
const diagnostics = this.#data.get(uri);
132
if (diagnostics) {
133
134
// no more than N diagnostics per file
135
if (diagnostics.length > this._maxDiagnosticsPerFile) {
136
marker = [];
137
const order = [DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint];
138
orderLoop: for (let i = 0; i < 4; i++) {
139
for (const diagnostic of diagnostics) {
140
if (diagnostic.severity === order[i]) {
141
const len = marker.push({ ...converter.Diagnostic.from(diagnostic), modelVersionId: this._modelVersionIdProvider(uri) });
142
if (len === this._maxDiagnosticsPerFile) {
143
break orderLoop;
144
}
145
}
146
}
147
}
148
149
// add 'signal' marker for showing omitted errors/warnings
150
marker.push({
151
severity: MarkerSeverity.Info,
152
message: localize({ key: 'limitHit', comment: ['amount of errors/warning skipped due to limits'] }, "Not showing {0} further errors and warnings.", diagnostics.length - this._maxDiagnosticsPerFile),
153
startLineNumber: marker[marker.length - 1].startLineNumber,
154
startColumn: marker[marker.length - 1].startColumn,
155
endLineNumber: marker[marker.length - 1].endLineNumber,
156
endColumn: marker[marker.length - 1].endColumn
157
});
158
} else {
159
marker = diagnostics.map(diag => ({ ...converter.Diagnostic.from(diag), modelVersionId: this._modelVersionIdProvider(uri) }));
160
}
161
}
162
163
entries.push([uri, marker]);
164
165
totalMarkerCount += marker.length;
166
if (totalMarkerCount > this._maxDiagnosticsTotal) {
167
// ignore markers that are above the limit
168
break;
169
}
170
}
171
this.#proxy.$changeMany(this._owner, entries);
172
}
173
174
delete(uri: vscode.Uri): void {
175
this._checkDisposed();
176
this.#onDidChangeDiagnostics.fire([uri]);
177
this.#data.delete(uri);
178
this.#proxy?.$changeMany(this._owner, [[uri, undefined]]);
179
}
180
181
clear(): void {
182
this._checkDisposed();
183
this.#onDidChangeDiagnostics.fire([...this.#data.keys()]);
184
this.#data.clear();
185
this.#proxy?.$clear(this._owner);
186
}
187
188
forEach(callback: (uri: URI, diagnostics: ReadonlyArray<vscode.Diagnostic>, collection: DiagnosticCollection) => any, thisArg?: any): void {
189
this._checkDisposed();
190
for (const [uri, values] of this) {
191
callback.call(thisArg, uri, values, this);
192
}
193
}
194
195
*[Symbol.iterator](): IterableIterator<[uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]]> {
196
this._checkDisposed();
197
for (const uri of this.#data.keys()) {
198
yield [uri, this.get(uri)];
199
}
200
}
201
202
get(uri: URI): ReadonlyArray<vscode.Diagnostic> {
203
this._checkDisposed();
204
const result = this.#data.get(uri);
205
if (Array.isArray(result)) {
206
return Object.freeze(result.slice(0));
207
}
208
return [];
209
}
210
211
has(uri: URI): boolean {
212
this._checkDisposed();
213
return Array.isArray(this.#data.get(uri));
214
}
215
216
private _checkDisposed() {
217
if (this._isDisposed) {
218
throw new Error('illegal state - object is disposed');
219
}
220
}
221
222
private static _compareIndexedTuplesByUri(a: [vscode.Uri, readonly vscode.Diagnostic[]], b: [vscode.Uri, readonly vscode.Diagnostic[]]): number {
223
if (a[0].toString() < b[0].toString()) {
224
return -1;
225
} else if (a[0].toString() > b[0].toString()) {
226
return 1;
227
} else {
228
return 0;
229
}
230
}
231
}
232
233
export class ExtHostDiagnostics implements ExtHostDiagnosticsShape {
234
235
private static _idPool: number = 0;
236
private static readonly _maxDiagnosticsPerFile: number = 1000;
237
private static readonly _maxDiagnosticsTotal: number = 1.1 * this._maxDiagnosticsPerFile;
238
239
private readonly _proxy: MainThreadDiagnosticsShape;
240
private readonly _collections = new Map<string, DiagnosticCollection>();
241
private readonly _onDidChangeDiagnostics = new DebounceEmitter<readonly vscode.Uri[]>({ merge: all => all.flat(), delay: 50 });
242
243
static _mapper(last: readonly vscode.Uri[]): { uris: readonly vscode.Uri[] } {
244
const map = new ResourceMap<vscode.Uri>();
245
for (const uri of last) {
246
map.set(uri, uri);
247
}
248
return { uris: Object.freeze(Array.from(map.values())) };
249
}
250
251
readonly onDidChangeDiagnostics: Event<vscode.DiagnosticChangeEvent> = Event.map(this._onDidChangeDiagnostics.event, ExtHostDiagnostics._mapper);
252
253
constructor(
254
mainContext: IMainContext,
255
@ILogService private readonly _logService: ILogService,
256
@IExtHostFileSystemInfo private readonly _fileSystemInfoService: IExtHostFileSystemInfo,
257
private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors,
258
) {
259
this._proxy = mainContext.getProxy(MainContext.MainThreadDiagnostics);
260
}
261
262
createDiagnosticCollection(extensionId: ExtensionIdentifier, name?: string): vscode.DiagnosticCollection {
263
264
const { _collections, _proxy, _onDidChangeDiagnostics, _logService, _fileSystemInfoService, _extHostDocumentsAndEditors } = this;
265
266
const loggingProxy = new class implements MainThreadDiagnosticsShape {
267
$changeMany(owner: string, entries: [UriComponents, IMarkerData[] | undefined][]): void {
268
_proxy.$changeMany(owner, entries);
269
_logService.trace('[DiagnosticCollection] change many (extension, owner, uris)', extensionId.value, owner, entries.length === 0 ? 'CLEARING' : entries);
270
}
271
$clear(owner: string): void {
272
_proxy.$clear(owner);
273
_logService.trace('[DiagnosticCollection] remove all (extension, owner)', extensionId.value, owner);
274
}
275
dispose(): void {
276
_proxy.dispose();
277
}
278
};
279
280
281
let owner: string;
282
if (!name) {
283
name = '_generated_diagnostic_collection_name_#' + ExtHostDiagnostics._idPool++;
284
owner = name;
285
} else if (!_collections.has(name)) {
286
owner = name;
287
} else {
288
this._logService.warn(`DiagnosticCollection with name '${name}' does already exist.`);
289
do {
290
owner = name + ExtHostDiagnostics._idPool++;
291
} while (_collections.has(owner));
292
}
293
294
const result = new class extends DiagnosticCollection {
295
constructor() {
296
super(
297
name!, owner,
298
ExtHostDiagnostics._maxDiagnosticsTotal,
299
ExtHostDiagnostics._maxDiagnosticsPerFile,
300
uri => _extHostDocumentsAndEditors.getDocument(uri)?.version,
301
_fileSystemInfoService.extUri, loggingProxy, _onDidChangeDiagnostics
302
);
303
_collections.set(owner, this);
304
}
305
override dispose() {
306
super.dispose();
307
_collections.delete(owner);
308
}
309
};
310
311
return result;
312
}
313
314
getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic>;
315
getDiagnostics(): ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>;
316
getDiagnostics(resource?: vscode.Uri): ReadonlyArray<vscode.Diagnostic> | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>;
317
getDiagnostics(resource?: vscode.Uri): ReadonlyArray<vscode.Diagnostic> | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]> {
318
if (resource) {
319
return this._getDiagnostics(resource);
320
} else {
321
const index = new Map<string, number>();
322
const res: [vscode.Uri, vscode.Diagnostic[]][] = [];
323
for (const collection of this._collections.values()) {
324
collection.forEach((uri, diagnostics) => {
325
let idx = index.get(uri.toString());
326
if (typeof idx === 'undefined') {
327
idx = res.length;
328
index.set(uri.toString(), idx);
329
res.push([uri, []]);
330
}
331
res[idx][1] = res[idx][1].concat(...diagnostics);
332
});
333
}
334
return res;
335
}
336
}
337
338
private _getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic> {
339
let res: vscode.Diagnostic[] = [];
340
for (const collection of this._collections.values()) {
341
if (collection.has(resource)) {
342
res = res.concat(collection.get(resource));
343
}
344
}
345
return res;
346
}
347
348
private _mirrorCollection: vscode.DiagnosticCollection | undefined;
349
350
$acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void {
351
352
if (!this._mirrorCollection) {
353
const name = '_generated_mirror';
354
const collection = new DiagnosticCollection(
355
name, name,
356
Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, // no limits because this collection is just a mirror of "sanitized" data
357
_uri => undefined,
358
this._fileSystemInfoService.extUri, undefined, this._onDidChangeDiagnostics
359
);
360
this._collections.set(name, collection);
361
this._mirrorCollection = collection;
362
}
363
364
for (const [uri, markers] of data) {
365
this._mirrorCollection.set(URI.revive(uri), markers.map(converter.Diagnostic.to));
366
}
367
}
368
}
369
370