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