Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/markers/common/markerService.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
import { isFalsyOrEmpty, isNonEmptyArray } from '../../../base/common/arrays.js';
7
import { DebounceEmitter } from '../../../base/common/event.js';
8
import { Iterable } from '../../../base/common/iterator.js';
9
import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10
import { ResourceMap, ResourceSet } from '../../../base/common/map.js';
11
import { Schemas } from '../../../base/common/network.js';
12
import { URI } from '../../../base/common/uri.js';
13
import { localize } from '../../../nls.js';
14
import { IMarker, IMarkerData, IMarkerReadOptions, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers.js';
15
16
export const unsupportedSchemas = new Set([
17
Schemas.inMemory,
18
Schemas.vscodeSourceControl,
19
Schemas.walkThrough,
20
Schemas.walkThroughSnippet,
21
Schemas.vscodeChatCodeBlock,
22
Schemas.vscodeTerminal
23
]);
24
25
class DoubleResourceMap<V> {
26
27
private _byResource = new ResourceMap<Map<string, V>>();
28
private _byOwner = new Map<string, ResourceMap<V>>();
29
30
set(resource: URI, owner: string, value: V) {
31
let ownerMap = this._byResource.get(resource);
32
if (!ownerMap) {
33
ownerMap = new Map();
34
this._byResource.set(resource, ownerMap);
35
}
36
ownerMap.set(owner, value);
37
38
let resourceMap = this._byOwner.get(owner);
39
if (!resourceMap) {
40
resourceMap = new ResourceMap();
41
this._byOwner.set(owner, resourceMap);
42
}
43
resourceMap.set(resource, value);
44
}
45
46
get(resource: URI, owner: string): V | undefined {
47
const ownerMap = this._byResource.get(resource);
48
return ownerMap?.get(owner);
49
}
50
51
delete(resource: URI, owner: string): boolean {
52
let removedA = false;
53
let removedB = false;
54
const ownerMap = this._byResource.get(resource);
55
if (ownerMap) {
56
removedA = ownerMap.delete(owner);
57
}
58
const resourceMap = this._byOwner.get(owner);
59
if (resourceMap) {
60
removedB = resourceMap.delete(resource);
61
}
62
if (removedA !== removedB) {
63
throw new Error('illegal state');
64
}
65
return removedA && removedB;
66
}
67
68
values(key?: URI | string): Iterable<V> {
69
if (typeof key === 'string') {
70
return this._byOwner.get(key)?.values() ?? Iterable.empty();
71
}
72
if (URI.isUri(key)) {
73
return this._byResource.get(key)?.values() ?? Iterable.empty();
74
}
75
76
return Iterable.map(Iterable.concat(...this._byOwner.values()), map => map[1]);
77
}
78
}
79
80
class MarkerStats implements MarkerStatistics {
81
82
errors: number = 0;
83
infos: number = 0;
84
warnings: number = 0;
85
unknowns: number = 0;
86
87
private readonly _data = new ResourceMap<MarkerStatistics>();
88
private readonly _service: IMarkerService;
89
private readonly _subscription: IDisposable;
90
91
constructor(service: IMarkerService) {
92
this._service = service;
93
this._subscription = service.onMarkerChanged(this._update, this);
94
}
95
96
dispose(): void {
97
this._subscription.dispose();
98
}
99
100
private _update(resources: readonly URI[]): void {
101
for (const resource of resources) {
102
const oldStats = this._data.get(resource);
103
if (oldStats) {
104
this._substract(oldStats);
105
}
106
const newStats = this._resourceStats(resource);
107
this._add(newStats);
108
this._data.set(resource, newStats);
109
}
110
}
111
112
private _resourceStats(resource: URI): MarkerStatistics {
113
const result: MarkerStatistics = { errors: 0, warnings: 0, infos: 0, unknowns: 0 };
114
115
// TODO this is a hack
116
if (unsupportedSchemas.has(resource.scheme)) {
117
return result;
118
}
119
120
for (const { severity } of this._service.read({ resource })) {
121
if (severity === MarkerSeverity.Error) {
122
result.errors += 1;
123
} else if (severity === MarkerSeverity.Warning) {
124
result.warnings += 1;
125
} else if (severity === MarkerSeverity.Info) {
126
result.infos += 1;
127
} else {
128
result.unknowns += 1;
129
}
130
}
131
132
return result;
133
}
134
135
private _substract(op: MarkerStatistics) {
136
this.errors -= op.errors;
137
this.warnings -= op.warnings;
138
this.infos -= op.infos;
139
this.unknowns -= op.unknowns;
140
}
141
142
private _add(op: MarkerStatistics) {
143
this.errors += op.errors;
144
this.warnings += op.warnings;
145
this.infos += op.infos;
146
this.unknowns += op.unknowns;
147
}
148
}
149
150
export class MarkerService implements IMarkerService {
151
152
declare readonly _serviceBrand: undefined;
153
154
private readonly _onMarkerChanged = new DebounceEmitter<readonly URI[]>({
155
delay: 0,
156
merge: MarkerService._merge
157
});
158
159
readonly onMarkerChanged = this._onMarkerChanged.event;
160
161
private readonly _data = new DoubleResourceMap<IMarker[]>();
162
private readonly _stats = new MarkerStats(this);
163
private readonly _filteredResources = new ResourceMap<string[]>();
164
165
dispose(): void {
166
this._stats.dispose();
167
this._onMarkerChanged.dispose();
168
}
169
170
getStatistics(): MarkerStatistics {
171
return this._stats;
172
}
173
174
remove(owner: string, resources: URI[]): void {
175
for (const resource of resources || []) {
176
this.changeOne(owner, resource, []);
177
}
178
}
179
180
changeOne(owner: string, resource: URI, markerData: IMarkerData[]): void {
181
182
if (isFalsyOrEmpty(markerData)) {
183
// remove marker for this (owner,resource)-tuple
184
const removed = this._data.delete(resource, owner);
185
if (removed) {
186
this._onMarkerChanged.fire([resource]);
187
}
188
189
} else {
190
// insert marker for this (owner,resource)-tuple
191
const markers: IMarker[] = [];
192
for (const data of markerData) {
193
const marker = MarkerService._toMarker(owner, resource, data);
194
if (marker) {
195
markers.push(marker);
196
}
197
}
198
this._data.set(resource, owner, markers);
199
this._onMarkerChanged.fire([resource]);
200
}
201
}
202
203
installResourceFilter(resource: URI, reason: string): IDisposable {
204
let reasons = this._filteredResources.get(resource);
205
206
if (!reasons) {
207
reasons = [];
208
this._filteredResources.set(resource, reasons);
209
}
210
reasons.push(reason);
211
this._onMarkerChanged.fire([resource]);
212
213
return toDisposable(() => {
214
const reasons = this._filteredResources.get(resource);
215
if (!reasons) {
216
return;
217
}
218
const reasonIndex = reasons.indexOf(reason);
219
if (reasonIndex !== -1) {
220
reasons.splice(reasonIndex, 1);
221
if (reasons.length === 0) {
222
this._filteredResources.delete(resource);
223
}
224
this._onMarkerChanged.fire([resource]);
225
}
226
});
227
}
228
229
private static _toMarker(owner: string, resource: URI, data: IMarkerData): IMarker | undefined {
230
let {
231
code, severity,
232
message, source,
233
startLineNumber, startColumn, endLineNumber, endColumn,
234
relatedInformation,
235
tags, origin
236
} = data;
237
238
if (!message) {
239
return undefined;
240
}
241
242
// santize data
243
startLineNumber = startLineNumber > 0 ? startLineNumber : 1;
244
startColumn = startColumn > 0 ? startColumn : 1;
245
endLineNumber = endLineNumber >= startLineNumber ? endLineNumber : startLineNumber;
246
endColumn = endColumn > 0 ? endColumn : startColumn;
247
248
return {
249
resource,
250
owner,
251
code,
252
severity,
253
message,
254
source,
255
startLineNumber,
256
startColumn,
257
endLineNumber,
258
endColumn,
259
relatedInformation,
260
tags,
261
origin
262
};
263
}
264
265
changeAll(owner: string, data: IResourceMarker[]): void {
266
const changes: URI[] = [];
267
268
// remove old marker
269
const existing = this._data.values(owner);
270
if (existing) {
271
for (const data of existing) {
272
const first = Iterable.first(data);
273
if (first) {
274
changes.push(first.resource);
275
this._data.delete(first.resource, owner);
276
}
277
}
278
}
279
280
// add new markers
281
if (isNonEmptyArray(data)) {
282
283
// group by resource
284
const groups = new ResourceMap<IMarker[]>();
285
for (const { resource, marker: markerData } of data) {
286
const marker = MarkerService._toMarker(owner, resource, markerData);
287
if (!marker) {
288
// filter bad markers
289
continue;
290
}
291
const array = groups.get(resource);
292
if (!array) {
293
groups.set(resource, [marker]);
294
changes.push(resource);
295
} else {
296
array.push(marker);
297
}
298
}
299
300
// insert all
301
for (const [resource, value] of groups) {
302
this._data.set(resource, owner, value);
303
}
304
}
305
306
if (changes.length > 0) {
307
this._onMarkerChanged.fire(changes);
308
}
309
}
310
311
/**
312
* Creates an information marker for filtered resources
313
*/
314
private _createFilteredMarker(resource: URI, reasons: string[]): IMarker {
315
const message = reasons.length === 1
316
? localize('filtered', "Problems are paused because: \"{0}\"", reasons[0])
317
: localize('filtered.network', "Problems are paused because: \"{0}\" and {1} more", reasons[0], reasons.length - 1);
318
319
return {
320
owner: 'markersFilter',
321
resource,
322
severity: MarkerSeverity.Info,
323
message,
324
startLineNumber: 1,
325
startColumn: 1,
326
endLineNumber: 1,
327
endColumn: 1,
328
};
329
}
330
331
read(filter: IMarkerReadOptions = Object.create(null)): IMarker[] {
332
333
let { owner, resource, severities, take } = filter;
334
335
if (!take || take < 0) {
336
take = -1;
337
}
338
339
if (owner && resource) {
340
// exactly one owner AND resource
341
const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined;
342
if (reasons?.length) {
343
const infoMarker = this._createFilteredMarker(resource, reasons);
344
return [infoMarker];
345
}
346
347
const data = this._data.get(resource, owner);
348
if (!data) {
349
return [];
350
}
351
352
const result: IMarker[] = [];
353
for (const marker of data) {
354
if (take > 0 && result.length === take) {
355
break;
356
}
357
const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined;
358
if (reasons?.length) {
359
result.push(this._createFilteredMarker(resource, reasons));
360
361
} else if (MarkerService._accept(marker, severities)) {
362
result.push(marker);
363
}
364
}
365
return result;
366
367
} else {
368
// of one resource OR owner
369
const iterable = !owner && !resource
370
? this._data.values()
371
: this._data.values(resource ?? owner!);
372
373
const result: IMarker[] = [];
374
const filtered = new ResourceSet();
375
376
for (const markers of iterable) {
377
for (const data of markers) {
378
if (filtered.has(data.resource)) {
379
continue;
380
}
381
if (take > 0 && result.length === take) {
382
break;
383
}
384
const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(data.resource) : undefined;
385
if (reasons?.length) {
386
result.push(this._createFilteredMarker(data.resource, reasons));
387
filtered.add(data.resource);
388
389
} else if (MarkerService._accept(data, severities)) {
390
result.push(data);
391
}
392
}
393
}
394
return result;
395
}
396
}
397
398
private static _accept(marker: IMarker, severities?: number): boolean {
399
return severities === undefined || (severities & marker.severity) === marker.severity;
400
}
401
402
// --- event debounce logic
403
404
private static _merge(all: (readonly URI[])[]): URI[] {
405
const set = new ResourceMap<boolean>();
406
for (const array of all) {
407
for (const item of array) {
408
set.set(item, true);
409
}
410
}
411
return Array.from(set.keys());
412
}
413
}
414
415