Path: blob/main/src/vs/platform/markers/common/markerService.ts
3296 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 { DebounceEmitter } 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 DebounceEmitter<readonly URI[]>({154delay: 0,155merge: MarkerService._merge156});157158readonly onMarkerChanged = this._onMarkerChanged.event;159160private readonly _data = new DoubleResourceMap<IMarker[]>();161private readonly _stats = new MarkerStats(this);162private readonly _filteredResources = new ResourceMap<string[]>();163164dispose(): void {165this._stats.dispose();166this._onMarkerChanged.dispose();167}168169getStatistics(): MarkerStatistics {170return this._stats;171}172173remove(owner: string, resources: URI[]): void {174for (const resource of resources || []) {175this.changeOne(owner, resource, []);176}177}178179changeOne(owner: string, resource: URI, markerData: IMarkerData[]): void {180181if (isFalsyOrEmpty(markerData)) {182// remove marker for this (owner,resource)-tuple183const removed = this._data.delete(resource, owner);184if (removed) {185this._onMarkerChanged.fire([resource]);186}187188} else {189// insert marker for this (owner,resource)-tuple190const markers: IMarker[] = [];191for (const data of markerData) {192const marker = MarkerService._toMarker(owner, resource, data);193if (marker) {194markers.push(marker);195}196}197this._data.set(resource, owner, markers);198this._onMarkerChanged.fire([resource]);199}200}201202installResourceFilter(resource: URI, reason: string): IDisposable {203let reasons = this._filteredResources.get(resource);204205if (!reasons) {206reasons = [];207this._filteredResources.set(resource, reasons);208}209reasons.push(reason);210this._onMarkerChanged.fire([resource]);211212return toDisposable(() => {213const reasons = this._filteredResources.get(resource);214if (!reasons) {215return;216}217const reasonIndex = reasons.indexOf(reason);218if (reasonIndex !== -1) {219reasons.splice(reasonIndex, 1);220if (reasons.length === 0) {221this._filteredResources.delete(resource);222}223this._onMarkerChanged.fire([resource]);224}225});226}227228private static _toMarker(owner: string, resource: URI, data: IMarkerData): IMarker | undefined {229let {230code, severity,231message, source,232startLineNumber, startColumn, endLineNumber, endColumn,233relatedInformation,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,259tags,260origin261};262}263264changeAll(owner: string, data: IResourceMarker[]): void {265const changes: URI[] = [];266267// remove old marker268const existing = this._data.values(owner);269if (existing) {270for (const data of existing) {271const first = Iterable.first(data);272if (first) {273changes.push(first.resource);274this._data.delete(first.resource, owner);275}276}277}278279// add new markers280if (isNonEmptyArray(data)) {281282// group by resource283const groups = new ResourceMap<IMarker[]>();284for (const { resource, marker: markerData } of data) {285const marker = MarkerService._toMarker(owner, resource, markerData);286if (!marker) {287// filter bad markers288continue;289}290const array = groups.get(resource);291if (!array) {292groups.set(resource, [marker]);293changes.push(resource);294} else {295array.push(marker);296}297}298299// insert all300for (const [resource, value] of groups) {301this._data.set(resource, owner, value);302}303}304305if (changes.length > 0) {306this._onMarkerChanged.fire(changes);307}308}309310/**311* Creates an information marker for filtered resources312*/313private _createFilteredMarker(resource: URI, reasons: string[]): IMarker {314const message = reasons.length === 1315? localize('filtered', "Problems are paused because: \"{0}\"", reasons[0])316: localize('filtered.network', "Problems are paused because: \"{0}\" and {1} more", reasons[0], reasons.length - 1);317318return {319owner: 'markersFilter',320resource,321severity: MarkerSeverity.Info,322message,323startLineNumber: 1,324startColumn: 1,325endLineNumber: 1,326endColumn: 1,327};328}329330read(filter: IMarkerReadOptions = Object.create(null)): IMarker[] {331332let { owner, resource, severities, take } = filter;333334if (!take || take < 0) {335take = -1;336}337338if (owner && resource) {339// exactly one owner AND resource340const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined;341if (reasons?.length) {342const infoMarker = this._createFilteredMarker(resource, reasons);343return [infoMarker];344}345346const data = this._data.get(resource, owner);347if (!data) {348return [];349}350351const result: IMarker[] = [];352for (const marker of data) {353if (take > 0 && result.length === take) {354break;355}356const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined;357if (reasons?.length) {358result.push(this._createFilteredMarker(resource, reasons));359360} else if (MarkerService._accept(marker, severities)) {361result.push(marker);362}363}364return result;365366} else {367// of one resource OR owner368const iterable = !owner && !resource369? this._data.values()370: this._data.values(resource ?? owner!);371372const result: IMarker[] = [];373const filtered = new ResourceSet();374375for (const markers of iterable) {376for (const data of markers) {377if (filtered.has(data.resource)) {378continue;379}380if (take > 0 && result.length === take) {381break;382}383const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(data.resource) : undefined;384if (reasons?.length) {385result.push(this._createFilteredMarker(data.resource, reasons));386filtered.add(data.resource);387388} else if (MarkerService._accept(data, severities)) {389result.push(data);390}391}392}393return result;394}395}396397private static _accept(marker: IMarker, severities?: number): boolean {398return severities === undefined || (severities & marker.severity) === marker.severity;399}400401// --- event debounce logic402403private static _merge(all: (readonly URI[])[]): URI[] {404const set = new ResourceMap<boolean>();405for (const array of all) {406for (const item of array) {407set.set(item, true);408}409}410return Array.from(set.keys());411}412}413414415