Path: blob/main/src/vs/workbench/api/common/extHostDiagnostics.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*--------------------------------------------------------------------------------------------*/45/* eslint-disable local/code-no-native-private */67import { localize } from '../../../nls.js';8import { IMarkerData, MarkerSeverity } from '../../../platform/markers/common/markers.js';9import { URI, UriComponents } from '../../../base/common/uri.js';10import type * as vscode from 'vscode';11import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from './extHost.protocol.js';12import { DiagnosticSeverity } from './extHostTypes.js';13import * as converter from './extHostTypeConverters.js';14import { Event, Emitter, DebounceEmitter } from '../../../base/common/event.js';15import { ILogService } from '../../../platform/log/common/log.js';16import { ResourceMap } from '../../../base/common/map.js';17import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';18import { IExtHostFileSystemInfo } from './extHostFileSystemInfo.js';19import { IExtUri } from '../../../base/common/resources.js';20import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';2122export class DiagnosticCollection implements vscode.DiagnosticCollection {2324readonly #proxy: MainThreadDiagnosticsShape | undefined;25readonly #onDidChangeDiagnostics: Emitter<readonly vscode.Uri[]>;26readonly #data: ResourceMap<vscode.Diagnostic[]>;2728private _isDisposed = false;2930constructor(31private readonly _name: string,32private readonly _owner: string,33private readonly _maxDiagnosticsTotal: number,34private readonly _maxDiagnosticsPerFile: number,35private readonly _modelVersionIdProvider: (uri: URI) => number | undefined,36extUri: IExtUri,37proxy: MainThreadDiagnosticsShape | undefined,38onDidChangeDiagnostics: Emitter<readonly vscode.Uri[]>39) {40this._maxDiagnosticsTotal = Math.max(_maxDiagnosticsPerFile, _maxDiagnosticsTotal);41this.#data = new ResourceMap(uri => extUri.getComparisonKey(uri));42this.#proxy = proxy;43this.#onDidChangeDiagnostics = onDidChangeDiagnostics;44}4546dispose(): void {47if (!this._isDisposed) {48this.#onDidChangeDiagnostics.fire([...this.#data.keys()]);49this.#proxy?.$clear(this._owner);50this.#data.clear();51this._isDisposed = true;52}53}5455get name(): string {56this._checkDisposed();57return this._name;58}5960set(uri: vscode.Uri, diagnostics: ReadonlyArray<vscode.Diagnostic>): void;61set(entries: ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>): void;62set(first: vscode.Uri | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>, diagnostics?: ReadonlyArray<vscode.Diagnostic>) {6364if (!first) {65// this set-call is a clear-call66this.clear();67return;68}6970// the actual implementation for #set7172this._checkDisposed();73let toSync: vscode.Uri[] = [];7475if (URI.isUri(first)) {7677if (!diagnostics) {78// remove this entry79this.delete(first);80return;81}8283// update single row84this.#data.set(first, diagnostics.slice());85toSync = [first];8687} else if (Array.isArray(first)) {88// update many rows89toSync = [];90let lastUri: vscode.Uri | undefined;9192// ensure stable-sort93first = [...first].sort(DiagnosticCollection._compareIndexedTuplesByUri);9495for (const tuple of first) {96const [uri, diagnostics] = tuple;97if (!lastUri || uri.toString() !== lastUri.toString()) {98if (lastUri && this.#data.get(lastUri)!.length === 0) {99this.#data.delete(lastUri);100}101lastUri = uri;102toSync.push(uri);103this.#data.set(uri, []);104}105106if (!diagnostics) {107// [Uri, undefined] means clear this108const currentDiagnostics = this.#data.get(uri);109if (currentDiagnostics) {110currentDiagnostics.length = 0;111}112} else {113const currentDiagnostics = this.#data.get(uri);114currentDiagnostics?.push(...diagnostics);115}116}117}118119// send event for extensions120this.#onDidChangeDiagnostics.fire(toSync);121122// compute change and send to main side123if (!this.#proxy) {124return;125}126const entries: [URI, IMarkerData[]][] = [];127let totalMarkerCount = 0;128for (const uri of toSync) {129let marker: IMarkerData[] = [];130const diagnostics = this.#data.get(uri);131if (diagnostics) {132133// no more than N diagnostics per file134if (diagnostics.length > this._maxDiagnosticsPerFile) {135marker = [];136const order = [DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint];137orderLoop: for (let i = 0; i < 4; i++) {138for (const diagnostic of diagnostics) {139if (diagnostic.severity === order[i]) {140const len = marker.push({ ...converter.Diagnostic.from(diagnostic), modelVersionId: this._modelVersionIdProvider(uri) });141if (len === this._maxDiagnosticsPerFile) {142break orderLoop;143}144}145}146}147148// add 'signal' marker for showing omitted errors/warnings149marker.push({150severity: MarkerSeverity.Info,151message: localize({ key: 'limitHit', comment: ['amount of errors/warning skipped due to limits'] }, "Not showing {0} further errors and warnings.", diagnostics.length - this._maxDiagnosticsPerFile),152startLineNumber: marker[marker.length - 1].startLineNumber,153startColumn: marker[marker.length - 1].startColumn,154endLineNumber: marker[marker.length - 1].endLineNumber,155endColumn: marker[marker.length - 1].endColumn156});157} else {158marker = diagnostics.map(diag => ({ ...converter.Diagnostic.from(diag), modelVersionId: this._modelVersionIdProvider(uri) }));159}160}161162entries.push([uri, marker]);163164totalMarkerCount += marker.length;165if (totalMarkerCount > this._maxDiagnosticsTotal) {166// ignore markers that are above the limit167break;168}169}170this.#proxy.$changeMany(this._owner, entries);171}172173delete(uri: vscode.Uri): void {174this._checkDisposed();175this.#onDidChangeDiagnostics.fire([uri]);176this.#data.delete(uri);177this.#proxy?.$changeMany(this._owner, [[uri, undefined]]);178}179180clear(): void {181this._checkDisposed();182this.#onDidChangeDiagnostics.fire([...this.#data.keys()]);183this.#data.clear();184this.#proxy?.$clear(this._owner);185}186187forEach(callback: (uri: URI, diagnostics: ReadonlyArray<vscode.Diagnostic>, collection: DiagnosticCollection) => any, thisArg?: any): void {188this._checkDisposed();189for (const [uri, values] of this) {190callback.call(thisArg, uri, values, this);191}192}193194*[Symbol.iterator](): IterableIterator<[uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]]> {195this._checkDisposed();196for (const uri of this.#data.keys()) {197yield [uri, this.get(uri)];198}199}200201get(uri: URI): ReadonlyArray<vscode.Diagnostic> {202this._checkDisposed();203const result = this.#data.get(uri);204if (Array.isArray(result)) {205return Object.freeze(result.slice(0));206}207return [];208}209210has(uri: URI): boolean {211this._checkDisposed();212return Array.isArray(this.#data.get(uri));213}214215private _checkDisposed() {216if (this._isDisposed) {217throw new Error('illegal state - object is disposed');218}219}220221private static _compareIndexedTuplesByUri(a: [vscode.Uri, readonly vscode.Diagnostic[]], b: [vscode.Uri, readonly vscode.Diagnostic[]]): number {222if (a[0].toString() < b[0].toString()) {223return -1;224} else if (a[0].toString() > b[0].toString()) {225return 1;226} else {227return 0;228}229}230}231232export class ExtHostDiagnostics implements ExtHostDiagnosticsShape {233234private static _idPool: number = 0;235private static readonly _maxDiagnosticsPerFile: number = 1000;236private static readonly _maxDiagnosticsTotal: number = 1.1 * this._maxDiagnosticsPerFile;237238private readonly _proxy: MainThreadDiagnosticsShape;239private readonly _collections = new Map<string, DiagnosticCollection>();240private readonly _onDidChangeDiagnostics = new DebounceEmitter<readonly vscode.Uri[]>({ merge: all => all.flat(), delay: 50 });241242static _mapper(last: readonly vscode.Uri[]): { uris: readonly vscode.Uri[] } {243const map = new ResourceMap<vscode.Uri>();244for (const uri of last) {245map.set(uri, uri);246}247return { uris: Object.freeze(Array.from(map.values())) };248}249250readonly onDidChangeDiagnostics: Event<vscode.DiagnosticChangeEvent> = Event.map(this._onDidChangeDiagnostics.event, ExtHostDiagnostics._mapper);251252constructor(253mainContext: IMainContext,254@ILogService private readonly _logService: ILogService,255@IExtHostFileSystemInfo private readonly _fileSystemInfoService: IExtHostFileSystemInfo,256private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors,257) {258this._proxy = mainContext.getProxy(MainContext.MainThreadDiagnostics);259}260261createDiagnosticCollection(extensionId: ExtensionIdentifier, name?: string): vscode.DiagnosticCollection {262263const { _collections, _proxy, _onDidChangeDiagnostics, _logService, _fileSystemInfoService, _extHostDocumentsAndEditors } = this;264265const loggingProxy = new class implements MainThreadDiagnosticsShape {266$changeMany(owner: string, entries: [UriComponents, IMarkerData[] | undefined][]): void {267_proxy.$changeMany(owner, entries);268_logService.trace('[DiagnosticCollection] change many (extension, owner, uris)', extensionId.value, owner, entries.length === 0 ? 'CLEARING' : entries);269}270$clear(owner: string): void {271_proxy.$clear(owner);272_logService.trace('[DiagnosticCollection] remove all (extension, owner)', extensionId.value, owner);273}274dispose(): void {275_proxy.dispose();276}277};278279280let owner: string;281if (!name) {282name = '_generated_diagnostic_collection_name_#' + ExtHostDiagnostics._idPool++;283owner = name;284} else if (!_collections.has(name)) {285owner = name;286} else {287this._logService.warn(`DiagnosticCollection with name '${name}' does already exist.`);288do {289owner = name + ExtHostDiagnostics._idPool++;290} while (_collections.has(owner));291}292293const result = new class extends DiagnosticCollection {294constructor() {295super(296name!, owner,297ExtHostDiagnostics._maxDiagnosticsTotal,298ExtHostDiagnostics._maxDiagnosticsPerFile,299uri => _extHostDocumentsAndEditors.getDocument(uri)?.version,300_fileSystemInfoService.extUri, loggingProxy, _onDidChangeDiagnostics301);302_collections.set(owner, this);303}304override dispose() {305super.dispose();306_collections.delete(owner);307}308};309310return result;311}312313getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic>;314getDiagnostics(): ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>;315getDiagnostics(resource?: vscode.Uri): ReadonlyArray<vscode.Diagnostic> | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>;316getDiagnostics(resource?: vscode.Uri): ReadonlyArray<vscode.Diagnostic> | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]> {317if (resource) {318return this._getDiagnostics(resource);319} else {320const index = new Map<string, number>();321const res: [vscode.Uri, vscode.Diagnostic[]][] = [];322for (const collection of this._collections.values()) {323collection.forEach((uri, diagnostics) => {324let idx = index.get(uri.toString());325if (typeof idx === 'undefined') {326idx = res.length;327index.set(uri.toString(), idx);328res.push([uri, []]);329}330res[idx][1] = res[idx][1].concat(...diagnostics);331});332}333return res;334}335}336337private _getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic> {338let res: vscode.Diagnostic[] = [];339for (const collection of this._collections.values()) {340if (collection.has(resource)) {341res = res.concat(collection.get(resource));342}343}344return res;345}346347private _mirrorCollection: vscode.DiagnosticCollection | undefined;348349$acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void {350351if (!this._mirrorCollection) {352const name = '_generated_mirror';353const collection = new DiagnosticCollection(354name, name,355Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, // no limits because this collection is just a mirror of "sanitized" data356_uri => undefined,357this._fileSystemInfoService.extUri, undefined, this._onDidChangeDiagnostics358);359this._collections.set(name, collection);360this._mirrorCollection = collection;361}362363for (const [uri, markers] of data) {364this._mirrorCollection.set(URI.revive(uri), markers.map(converter.Diagnostic.to));365}366}367}368369370