Path: blob/main/src/vs/platform/markers/common/markerService.ts
5243 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { isFalsyOrEmpty, isNonEmptyArray } from '../../../base/common/arrays.js';6import { MicrotaskEmitter } from '../../../base/common/event.js';7import { Iterable } from '../../../base/common/iterator.js';8import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';9import { ResourceMap, ResourceSet } from '../../../base/common/map.js';10import { Schemas } from '../../../base/common/network.js';11import { URI } from '../../../base/common/uri.js';12import { localize } from '../../../nls.js';13import { IMarker, IMarkerData, IMarkerReadOptions, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers.js';1415export const unsupportedSchemas = new Set([16Schemas.inMemory,17Schemas.vscodeSourceControl,18Schemas.walkThrough,19Schemas.walkThroughSnippet,20Schemas.vscodeChatCodeBlock,21Schemas.vscodeTerminal22]);2324class DoubleResourceMap<V> {2526private _byResource = new ResourceMap<Map<string, V>>();27private _byOwner = new Map<string, ResourceMap<V>>();2829set(resource: URI, owner: string, value: V) {30let ownerMap = this._byResource.get(resource);31if (!ownerMap) {32ownerMap = new Map();33this._byResource.set(resource, ownerMap);34}35ownerMap.set(owner, value);3637let resourceMap = this._byOwner.get(owner);38if (!resourceMap) {39resourceMap = new ResourceMap();40this._byOwner.set(owner, resourceMap);41}42resourceMap.set(resource, value);43}4445get(resource: URI, owner: string): V | undefined {46const ownerMap = this._byResource.get(resource);47return ownerMap?.get(owner);48}4950delete(resource: URI, owner: string): boolean {51let removedA = false;52let removedB = false;53const ownerMap = this._byResource.get(resource);54if (ownerMap) {55removedA = ownerMap.delete(owner);56}57const resourceMap = this._byOwner.get(owner);58if (resourceMap) {59removedB = resourceMap.delete(resource);60}61if (removedA !== removedB) {62throw new Error('illegal state');63}64return removedA && removedB;65}6667values(key?: URI | string): Iterable<V> {68if (typeof key === 'string') {69return this._byOwner.get(key)?.values() ?? Iterable.empty();70}71if (URI.isUri(key)) {72return this._byResource.get(key)?.values() ?? Iterable.empty();73}7475return Iterable.map(Iterable.concat(...this._byOwner.values()), map => map[1]);76}77}7879class MarkerStats implements MarkerStatistics {8081errors: number = 0;82infos: number = 0;83warnings: number = 0;84unknowns: number = 0;8586private readonly _data = new ResourceMap<MarkerStatistics>();87private readonly _service: IMarkerService;88private readonly _subscription: IDisposable;8990constructor(service: IMarkerService) {91this._service = service;92this._subscription = service.onMarkerChanged(this._update, this);93}9495dispose(): void {96this._subscription.dispose();97}9899private _update(resources: readonly URI[]): void {100for (const resource of resources) {101const oldStats = this._data.get(resource);102if (oldStats) {103this._substract(oldStats);104}105const newStats = this._resourceStats(resource);106this._add(newStats);107this._data.set(resource, newStats);108}109}110111private _resourceStats(resource: URI): MarkerStatistics {112const result: MarkerStatistics = { errors: 0, warnings: 0, infos: 0, unknowns: 0 };113114// TODO this is a hack115if (unsupportedSchemas.has(resource.scheme)) {116return result;117}118119for (const { severity } of this._service.read({ resource })) {120if (severity === MarkerSeverity.Error) {121result.errors += 1;122} else if (severity === MarkerSeverity.Warning) {123result.warnings += 1;124} else if (severity === MarkerSeverity.Info) {125result.infos += 1;126} else {127result.unknowns += 1;128}129}130131return result;132}133134private _substract(op: MarkerStatistics) {135this.errors -= op.errors;136this.warnings -= op.warnings;137this.infos -= op.infos;138this.unknowns -= op.unknowns;139}140141private _add(op: MarkerStatistics) {142this.errors += op.errors;143this.warnings += op.warnings;144this.infos += op.infos;145this.unknowns += op.unknowns;146}147}148149export class MarkerService implements IMarkerService {150151declare readonly _serviceBrand: undefined;152153private readonly _onMarkerChanged = new MicrotaskEmitter<readonly URI[]>({154merge: MarkerService._merge155});156157readonly onMarkerChanged = this._onMarkerChanged.event;158159private readonly _data = new DoubleResourceMap<IMarker[]>();160private readonly _stats = new MarkerStats(this);161private readonly _filteredResources = new ResourceMap<string[]>();162163dispose(): void {164this._stats.dispose();165this._onMarkerChanged.dispose();166}167168getStatistics(): MarkerStatistics {169return this._stats;170}171172remove(owner: string, resources: URI[]): void {173for (const resource of resources || []) {174this.changeOne(owner, resource, []);175}176}177178changeOne(owner: string, resource: URI, markerData: IMarkerData[]): void {179180if (isFalsyOrEmpty(markerData)) {181// remove marker for this (owner,resource)-tuple182const removed = this._data.delete(resource, owner);183if (removed) {184this._onMarkerChanged.fire([resource]);185}186187} else {188// insert marker for this (owner,resource)-tuple189const markers: IMarker[] = [];190for (const data of markerData) {191const marker = MarkerService._toMarker(owner, resource, data);192if (marker) {193markers.push(marker);194}195}196this._data.set(resource, owner, markers);197this._onMarkerChanged.fire([resource]);198}199}200201installResourceFilter(resource: URI, reason: string): IDisposable {202let reasons = this._filteredResources.get(resource);203204if (!reasons) {205reasons = [];206this._filteredResources.set(resource, reasons);207}208reasons.push(reason);209this._onMarkerChanged.fire([resource]);210211return toDisposable(() => {212const reasons = this._filteredResources.get(resource);213if (!reasons) {214return;215}216const reasonIndex = reasons.indexOf(reason);217if (reasonIndex !== -1) {218reasons.splice(reasonIndex, 1);219if (reasons.length === 0) {220this._filteredResources.delete(resource);221}222this._onMarkerChanged.fire([resource]);223}224});225}226227private static _toMarker(owner: string, resource: URI, data: IMarkerData): IMarker | undefined {228let {229code, severity,230message, source,231startLineNumber, startColumn, endLineNumber, endColumn,232relatedInformation,233modelVersionId,234tags, origin235} = data;236237if (!message) {238return undefined;239}240241// santize data242startLineNumber = startLineNumber > 0 ? startLineNumber : 1;243startColumn = startColumn > 0 ? startColumn : 1;244endLineNumber = endLineNumber >= startLineNumber ? endLineNumber : startLineNumber;245endColumn = endColumn > 0 ? endColumn : startColumn;246247return {248resource,249owner,250code,251severity,252message,253source,254startLineNumber,255startColumn,256endLineNumber,257endColumn,258relatedInformation,259modelVersionId,260tags,261origin262};263}264265changeAll(owner: string, data: IResourceMarker[]): void {266const changes: URI[] = [];267268// remove old marker269const existing = this._data.values(owner);270if (existing) {271for (const data of existing) {272const first = Iterable.first(data);273if (first) {274changes.push(first.resource);275this._data.delete(first.resource, owner);276}277}278}279280// add new markers281if (isNonEmptyArray(data)) {282283// group by resource284const groups = new ResourceMap<IMarker[]>();285for (const { resource, marker: markerData } of data) {286const marker = MarkerService._toMarker(owner, resource, markerData);287if (!marker) {288// filter bad markers289continue;290}291const array = groups.get(resource);292if (!array) {293groups.set(resource, [marker]);294changes.push(resource);295} else {296array.push(marker);297}298}299300// insert all301for (const [resource, value] of groups) {302this._data.set(resource, owner, value);303}304}305306if (changes.length > 0) {307this._onMarkerChanged.fire(changes);308}309}310311/**312* Creates an information marker for filtered resources313*/314private _createFilteredMarker(resource: URI, reasons: string[]): IMarker {315const message = reasons.length === 1316? 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);318319return {320owner: 'markersFilter',321resource,322severity: MarkerSeverity.Info,323message,324startLineNumber: 1,325startColumn: 1,326endLineNumber: 1,327endColumn: 1,328};329}330331read(filter: IMarkerReadOptions = Object.create(null)): IMarker[] {332333let { owner, resource, severities, take } = filter;334335if (!take || take < 0) {336take = -1;337}338339if (owner && resource) {340// exactly one owner AND resource341const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined;342if (reasons?.length) {343const infoMarker = this._createFilteredMarker(resource, reasons);344return [infoMarker];345}346347const data = this._data.get(resource, owner);348if (!data) {349return [];350}351352const result: IMarker[] = [];353for (const marker of data) {354if (take > 0 && result.length === take) {355break;356}357const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined;358if (reasons?.length) {359result.push(this._createFilteredMarker(resource, reasons));360361} else if (MarkerService._accept(marker, severities)) {362result.push(marker);363}364}365return result;366367} else {368// of one resource OR owner369const iterable = !owner && !resource370? this._data.values()371: this._data.values(resource ?? owner!);372373const result: IMarker[] = [];374const filtered = new ResourceSet();375376for (const markers of iterable) {377for (const data of markers) {378if (filtered.has(data.resource)) {379continue;380}381if (take > 0 && result.length === take) {382break;383}384const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(data.resource) : undefined;385if (reasons?.length) {386result.push(this._createFilteredMarker(data.resource, reasons));387filtered.add(data.resource);388389} else if (MarkerService._accept(data, severities)) {390result.push(data);391}392}393}394return result;395}396}397398private static _accept(marker: IMarker, severities?: number): boolean {399return severities === undefined || (severities & marker.severity) === marker.severity;400}401402// --- event debounce logic403404private static _merge(all: (readonly URI[])[]): URI[] {405const set = new ResourceMap<boolean>();406for (const array of all) {407for (const item of array) {408set.set(item, true);409}410}411return Array.from(set.keys());412}413}414415416