Path: blob/main/src/vs/workbench/api/common/extHostDiagnostics.ts
5252 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 { coalesce } from '../../../base/common/arrays.js';16import { ILogService } from '../../../platform/log/common/log.js';17import { ResourceMap } from '../../../base/common/map.js';18import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';19import { IExtHostFileSystemInfo } from './extHostFileSystemInfo.js';20import { IExtUri } from '../../../base/common/resources.js';21import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';2223export class DiagnosticCollection implements vscode.DiagnosticCollection {2425readonly #proxy: MainThreadDiagnosticsShape | undefined;26readonly #onDidChangeDiagnostics: Emitter<readonly vscode.Uri[]>;27readonly #data: ResourceMap<vscode.Diagnostic[]>;2829private _isDisposed = false;3031constructor(32private readonly _name: string,33private readonly _owner: string,34private readonly _maxDiagnosticsTotal: number,35private readonly _maxDiagnosticsPerFile: number,36private readonly _modelVersionIdProvider: (uri: URI) => number | undefined,37extUri: IExtUri,38proxy: MainThreadDiagnosticsShape | undefined,39onDidChangeDiagnostics: Emitter<readonly vscode.Uri[]>40) {41this._maxDiagnosticsTotal = Math.max(_maxDiagnosticsPerFile, _maxDiagnosticsTotal);42this.#data = new ResourceMap(uri => extUri.getComparisonKey(uri));43this.#proxy = proxy;44this.#onDidChangeDiagnostics = onDidChangeDiagnostics;45}4647dispose(): void {48if (!this._isDisposed) {49this.#onDidChangeDiagnostics.fire([...this.#data.keys()]);50this.#proxy?.$clear(this._owner);51this.#data.clear();52this._isDisposed = true;53}54}5556get name(): string {57this._checkDisposed();58return this._name;59}6061set(uri: vscode.Uri, diagnostics: ReadonlyArray<vscode.Diagnostic>): void;62set(entries: ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>): void;63set(first: vscode.Uri | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>, diagnostics?: ReadonlyArray<vscode.Diagnostic>) {6465if (!first) {66// this set-call is a clear-call67this.clear();68return;69}7071// the actual implementation for #set7273this._checkDisposed();74let toSync: vscode.Uri[] = [];7576if (URI.isUri(first)) {7778if (!diagnostics) {79// remove this entry80this.delete(first);81return;82}8384// update single row85this.#data.set(first, coalesce(diagnostics));86toSync = [first];8788} else if (Array.isArray(first)) {89// update many rows90toSync = [];91let lastUri: vscode.Uri | undefined;9293// ensure stable-sort94first = [...first].sort(DiagnosticCollection._compareIndexedTuplesByUri);9596for (const tuple of first) {97const [uri, diagnostics] = tuple;98if (!lastUri || uri.toString() !== lastUri.toString()) {99if (lastUri && this.#data.get(lastUri)!.length === 0) {100this.#data.delete(lastUri);101}102lastUri = uri;103toSync.push(uri);104this.#data.set(uri, []);105}106107if (!diagnostics) {108// [Uri, undefined] means clear this109const currentDiagnostics = this.#data.get(uri);110if (currentDiagnostics) {111currentDiagnostics.length = 0;112}113} else {114const currentDiagnostics = this.#data.get(uri);115currentDiagnostics?.push(...coalesce(diagnostics));116}117}118}119120// send event for extensions121this.#onDidChangeDiagnostics.fire(toSync);122123// compute change and send to main side124if (!this.#proxy) {125return;126}127const entries: [URI, IMarkerData[]][] = [];128let totalMarkerCount = 0;129for (const uri of toSync) {130let marker: IMarkerData[] = [];131const diagnostics = this.#data.get(uri);132if (diagnostics) {133134// no more than N diagnostics per file135if (diagnostics.length > this._maxDiagnosticsPerFile) {136marker = [];137const order = [DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint];138orderLoop: for (let i = 0; i < 4; i++) {139for (const diagnostic of diagnostics) {140if (diagnostic.severity === order[i]) {141const len = marker.push({ ...converter.Diagnostic.from(diagnostic), modelVersionId: this._modelVersionIdProvider(uri) });142if (len === this._maxDiagnosticsPerFile) {143break orderLoop;144}145}146}147}148149// add 'signal' marker for showing omitted errors/warnings150marker.push({151severity: MarkerSeverity.Info,152message: localize({ key: 'limitHit', comment: ['amount of errors/warning skipped due to limits'] }, "Not showing {0} further errors and warnings.", diagnostics.length - this._maxDiagnosticsPerFile),153startLineNumber: marker[marker.length - 1].startLineNumber,154startColumn: marker[marker.length - 1].startColumn,155endLineNumber: marker[marker.length - 1].endLineNumber,156endColumn: marker[marker.length - 1].endColumn157});158} else {159marker = diagnostics.map(diag => ({ ...converter.Diagnostic.from(diag), modelVersionId: this._modelVersionIdProvider(uri) }));160}161}162163entries.push([uri, marker]);164165totalMarkerCount += marker.length;166if (totalMarkerCount > this._maxDiagnosticsTotal) {167// ignore markers that are above the limit168break;169}170}171this.#proxy.$changeMany(this._owner, entries);172}173174delete(uri: vscode.Uri): void {175this._checkDisposed();176this.#onDidChangeDiagnostics.fire([uri]);177this.#data.delete(uri);178this.#proxy?.$changeMany(this._owner, [[uri, undefined]]);179}180181clear(): void {182this._checkDisposed();183this.#onDidChangeDiagnostics.fire([...this.#data.keys()]);184this.#data.clear();185this.#proxy?.$clear(this._owner);186}187188forEach(callback: (uri: URI, diagnostics: ReadonlyArray<vscode.Diagnostic>, collection: DiagnosticCollection) => unknown, thisArg?: unknown): void {189this._checkDisposed();190for (const [uri, values] of this) {191callback.call(thisArg, uri, values, this);192}193}194195*[Symbol.iterator](): IterableIterator<[uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]]> {196this._checkDisposed();197for (const uri of this.#data.keys()) {198yield [uri, this.get(uri)];199}200}201202get(uri: URI): ReadonlyArray<vscode.Diagnostic> {203this._checkDisposed();204const result = this.#data.get(uri);205if (Array.isArray(result)) {206return Object.freeze(result.slice(0));207}208return [];209}210211has(uri: URI): boolean {212this._checkDisposed();213return Array.isArray(this.#data.get(uri));214}215216private _checkDisposed() {217if (this._isDisposed) {218throw new Error('illegal state - object is disposed');219}220}221222private static _compareIndexedTuplesByUri(a: [vscode.Uri, readonly vscode.Diagnostic[]], b: [vscode.Uri, readonly vscode.Diagnostic[]]): number {223if (a[0].toString() < b[0].toString()) {224return -1;225} else if (a[0].toString() > b[0].toString()) {226return 1;227} else {228return 0;229}230}231}232233export class ExtHostDiagnostics implements ExtHostDiagnosticsShape {234235private static _idPool: number = 0;236private static readonly _maxDiagnosticsPerFile: number = 1000;237private static readonly _maxDiagnosticsTotal: number = 1.1 * this._maxDiagnosticsPerFile;238239private readonly _proxy: MainThreadDiagnosticsShape;240private readonly _collections = new Map<string, DiagnosticCollection>();241private readonly _onDidChangeDiagnostics = new DebounceEmitter<readonly vscode.Uri[]>({ merge: all => all.flat(), delay: 50 });242243static _mapper(last: readonly vscode.Uri[]): { uris: readonly vscode.Uri[] } {244const map = new ResourceMap<vscode.Uri>();245for (const uri of last) {246map.set(uri, uri);247}248return { uris: Object.freeze(Array.from(map.values())) };249}250251readonly onDidChangeDiagnostics: Event<vscode.DiagnosticChangeEvent> = Event.map(this._onDidChangeDiagnostics.event, ExtHostDiagnostics._mapper);252253constructor(254mainContext: IMainContext,255@ILogService private readonly _logService: ILogService,256@IExtHostFileSystemInfo private readonly _fileSystemInfoService: IExtHostFileSystemInfo,257private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors,258) {259this._proxy = mainContext.getProxy(MainContext.MainThreadDiagnostics);260}261262createDiagnosticCollection(extensionId: ExtensionIdentifier, name?: string): vscode.DiagnosticCollection {263264const { _collections, _proxy, _onDidChangeDiagnostics, _logService, _fileSystemInfoService, _extHostDocumentsAndEditors } = this;265266const loggingProxy = new class implements MainThreadDiagnosticsShape {267$changeMany(owner: string, entries: [UriComponents, IMarkerData[] | undefined][]): void {268_proxy.$changeMany(owner, entries);269_logService.trace('[DiagnosticCollection] change many (extension, owner, uris)', extensionId.value, owner, entries.length === 0 ? 'CLEARING' : entries);270}271$clear(owner: string): void {272_proxy.$clear(owner);273_logService.trace('[DiagnosticCollection] remove all (extension, owner)', extensionId.value, owner);274}275dispose(): void {276_proxy.dispose();277}278};279280281let owner: string;282if (!name) {283name = '_generated_diagnostic_collection_name_#' + ExtHostDiagnostics._idPool++;284owner = name;285} else if (!_collections.has(name)) {286owner = name;287} else {288this._logService.warn(`DiagnosticCollection with name '${name}' does already exist.`);289do {290owner = name + ExtHostDiagnostics._idPool++;291} while (_collections.has(owner));292}293294const result = new class extends DiagnosticCollection {295constructor() {296super(297name!, owner,298ExtHostDiagnostics._maxDiagnosticsTotal,299ExtHostDiagnostics._maxDiagnosticsPerFile,300uri => _extHostDocumentsAndEditors.getDocument(uri)?.version,301_fileSystemInfoService.extUri, loggingProxy, _onDidChangeDiagnostics302);303_collections.set(owner, this);304}305override dispose() {306super.dispose();307_collections.delete(owner);308}309};310311return result;312}313314getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic>;315getDiagnostics(): ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>;316getDiagnostics(resource?: vscode.Uri): ReadonlyArray<vscode.Diagnostic> | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]>;317getDiagnostics(resource?: vscode.Uri): ReadonlyArray<vscode.Diagnostic> | ReadonlyArray<[vscode.Uri, ReadonlyArray<vscode.Diagnostic>]> {318if (resource) {319return this._getDiagnostics(resource);320} else {321const index = new Map<string, number>();322const res: [vscode.Uri, vscode.Diagnostic[]][] = [];323for (const collection of this._collections.values()) {324collection.forEach((uri, diagnostics) => {325let idx = index.get(uri.toString());326if (typeof idx === 'undefined') {327idx = res.length;328index.set(uri.toString(), idx);329res.push([uri, []]);330}331res[idx][1] = res[idx][1].concat(...diagnostics);332});333}334return res;335}336}337338private _getDiagnostics(resource: vscode.Uri): ReadonlyArray<vscode.Diagnostic> {339let res: vscode.Diagnostic[] = [];340for (const collection of this._collections.values()) {341if (collection.has(resource)) {342res = res.concat(collection.get(resource));343}344}345return res;346}347348private _mirrorCollection: vscode.DiagnosticCollection | undefined;349350$acceptMarkersChange(data: [UriComponents, IMarkerData[]][]): void {351352if (!this._mirrorCollection) {353const name = '_generated_mirror';354const collection = new DiagnosticCollection(355name, name,356Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, // no limits because this collection is just a mirror of "sanitized" data357_uri => undefined,358this._fileSystemInfoService.extUri, undefined, this._onDidChangeDiagnostics359);360this._collections.set(name, collection);361this._mirrorCollection = collection;362}363364for (const [uri, markers] of data) {365this._mirrorCollection.set(URI.revive(uri), markers.map(converter.Diagnostic.to));366}367}368}369370371