Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsCompletionProcessor.ts
13405 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 * as vscode from 'vscode';6import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';7import { applyEditsToRanges } from '../../../../platform/editSurvivalTracking/common/editSurvivalTracker';8import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';9import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';10import { Edits } from '../../../../platform/inlineEdits/common/dataTypes/edit';11import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit';12import { IObservableDocument } from '../../../../platform/inlineEdits/common/observableWorkspace';13import { autorunWithChanges } from '../../../../platform/inlineEdits/common/utils/observable';14import { WorkspaceDocumentEditHistory } from '../../../../platform/inlineEdits/common/workspaceEditTracker/workspaceDocumentEditTracker';15import { ILogger, ILogService } from '../../../../platform/log/common/logService';16import { ITabsAndEditorsService } from '../../../../platform/tabs/common/tabsAndEditorsService';17import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';18import { isNotebookCell } from '../../../../util/common/notebooks';19import { equals } from '../../../../util/vs/base/common/arrays';20import { findFirstMonotonous } from '../../../../util/vs/base/common/arraysFind';21import { ThrottledDelayer } from '../../../../util/vs/base/common/async';22import { CancellationToken, CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';23import { BugIndicatingError } from '../../../../util/vs/base/common/errors';24import { Emitter } from '../../../../util/vs/base/common/event';25import { Disposable } from '../../../../util/vs/base/common/lifecycle';26import { autorun, derived, IObservable, runOnChange } from '../../../../util/vs/base/common/observableInternal';27import { isEqual } from '../../../../util/vs/base/common/resources';28import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';29import { Position } from '../../../../util/vs/editor/common/core/position';30import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';31import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';32import { getInformationDelta, InformationDelta } from '../../common/informationDelta';33import { RejectionCollector } from '../../common/rejectionCollector';34import { IVSCodeObservableDocument, VSCodeWorkspace } from '../parts/vscodeWorkspace';35import { toInternalPosition } from '../utils/translations';36import { AnyDiagnosticCompletionItem, AnyDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/anyDiagnosticsCompletionProvider';37import { AsyncDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/asyncDiagnosticsCompletionProvider';38import { Diagnostic, DiagnosticCompletionItem, DiagnosticInlineEditRequestLogContext, IDiagnosticCompletionProvider, log, logList, sortDiagnosticsByDistance } from './diagnosticsBasedCompletions/diagnosticsCompletions';39import { ImportDiagnosticCompletionItem, ImportDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/importDiagnosticsCompletionProvider';4041interface IDiagnosticsCompletionState<T extends DiagnosticCompletionItem = DiagnosticCompletionItem> {42completionItem: T | null;43logContext: DiagnosticInlineEditRequestLogContext;44telemetryBuilder: DiagnosticsCompletionHandlerTelemetry;45}4647function diagnosticCompletionRunResultEquals(a: IDiagnosticsCompletionState, b: IDiagnosticsCompletionState): boolean {48if (!!a.completionItem && !!b.completionItem) {49return DiagnosticCompletionItem.equals(a.completionItem, b.completionItem);50}51return a.completionItem === b.completionItem;52}5354// Only exported for testing55export class DiagnosticsCollection {5657private _diagnostics: Diagnostic[] = [];5859applyEdit(previous: StringText, edit: StringEdit, after: StringText): boolean {6061let hasInvalidated = false;62for (const diagnostic of this._diagnostics) {63const oldRange = diagnostic.range;64const newRange = applyEditsToRanges([oldRange], edit)[0];6566// If the range shrank then the diagnostic will have changed67if (!newRange || newRange.length < oldRange.length) {68diagnostic.invalidate();69hasInvalidated = true;70continue;71}7273const contentAtOldRange = oldRange.substring(previous.value);7475// If the range stays the same then the diagnostic is still valid if the content is the same76if (newRange.length === oldRange.length) {77const contentAtNewRange = newRange.substring(after.value);78if (contentAtOldRange === contentAtNewRange) {79diagnostic.updateRange(newRange);80} else {81diagnostic.invalidate();82hasInvalidated = true;83}84continue;85}8687// If the range grew then we need to check what got added88const isSamePrefix = contentAtOldRange === new OffsetRange(newRange.start, newRange.start + oldRange.length).substring(after.value);89const isSameSuffix = contentAtOldRange === new OffsetRange(newRange.endExclusive - oldRange.length, newRange.endExclusive).substring(after.value);90if (!isSamePrefix && !isSameSuffix) {91// The content at the diagnostic range has changed92diagnostic.invalidate();93hasInvalidated = true;94continue;95}9697let edgeCharacter;98if (isSamePrefix) {99const offsetAfterOldRange = newRange.start + oldRange.length;100edgeCharacter = new OffsetRange(offsetAfterOldRange, offsetAfterOldRange + 1).substring(after.value);101} else {102const offsetBeforeOldRange = newRange.endExclusive - oldRange.length - 1;103edgeCharacter = new OffsetRange(offsetBeforeOldRange, offsetBeforeOldRange + 1).substring(after.value);104}105106if (edgeCharacter.length !== 1 || /^[a-zA-Z0-9_]$/.test(edgeCharacter)) {107// The content at the diagnostic range has changed108diagnostic.invalidate();109hasInvalidated = true;110continue;111}112113// We need to update the range of the diagnostic after applying the edits114let updatedRange: OffsetRange;115if (isSamePrefix) {116updatedRange = new OffsetRange(newRange.start, newRange.start + oldRange.length);117} else {118updatedRange = new OffsetRange(newRange.endExclusive - oldRange.length, newRange.endExclusive);119}120121diagnostic.updateRange(updatedRange);122}123124return hasInvalidated;125}126127isEqualAndUpdate(relevantDiagnostics: Diagnostic[]): boolean {128if (equals(this._diagnostics, relevantDiagnostics, Diagnostic.equals)) {129return true;130}131this._diagnostics = relevantDiagnostics;132return false;133}134135toString(): string {136return this._diagnostics.map(d => d.toString()).join('\n');137}138}139140export type DiagnosticCompletionState = {141item: DiagnosticCompletionItem | undefined;142telemetry: IDiagnosticsCompletionTelemetry;143logContext: DiagnosticInlineEditRequestLogContext | undefined;144workInProgress?: boolean;145};146147export class DiagnosticsCompletionProcessor extends Disposable {148149static get documentSelector(): vscode.DocumentSelector {150return Array.from(new Set([151...ImportDiagnosticCompletionProvider.SupportedLanguages,152...AsyncDiagnosticCompletionProvider.SupportedLanguages153]));154}155156private readonly _onDidChange = this._register(new Emitter<boolean>());157readonly onDidChange = this._onDidChange.event;158159private readonly _worker = new AsyncWorker<IDiagnosticsCompletionState>(20, diagnosticCompletionRunResultEquals);160161private readonly _rejectionCollector: RejectionCollector;162private readonly _diagnosticsCompletionProviders: IObservable<IDiagnosticCompletionProvider[]>;163private readonly _workspaceDocumentEditHistory: WorkspaceDocumentEditHistory;164private readonly _currentDiagnostics = new DiagnosticsCollection();165166private readonly _logger: ILogger;167168constructor(169private readonly _workspace: VSCodeWorkspace,170git: ObservableGit,171@ILogService logService: ILogService,172@IConfigurationService configurationService: IConfigurationService,173@IWorkspaceService workspaceService: IWorkspaceService,174@IFileSystemService fileSystemService: IFileSystemService,175@ITabsAndEditorsService private readonly _tabsAndEditorsService: ITabsAndEditorsService,176) {177super();178179this._workspaceDocumentEditHistory = this._register(new WorkspaceDocumentEditHistory(this._workspace, git, 100));180181this._logger = logService.createSubLogger(['NES', 'DiagnosticsInlineCompletionProvider']);182183const diagnosticsExplorationEnabled = configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsDiagnosticsExplorationEnabled);184185const importProvider = new ImportDiagnosticCompletionProvider(this._logger.createSubLogger('Import'), workspaceService, fileSystemService);186const asyncProvider = new AsyncDiagnosticCompletionProvider(this._logger.createSubLogger('Async'));187188this._diagnosticsCompletionProviders = derived(reader => {189const providers: IDiagnosticCompletionProvider[] = [190importProvider,191asyncProvider192];193194if (diagnosticsExplorationEnabled.read(reader)) {195providers.push(new AnyDiagnosticCompletionProvider(this._logger.createSubLogger('All')));196}197198return providers;199}).recomputeInitiallyAndOnChange(this._store);200201this._rejectionCollector = this._register(new RejectionCollector(this._workspace, logService));202203const isValidEditor = (editor: vscode.TextEditor | undefined): editor is vscode.TextEditor => {204return !!editor && (isNotebookCell(editor.document.uri) || isEditorFromEditorGrid(editor));205};206207this._register(autorun(reader => {208const activeDocument = this._workspace.lastActiveDocument.read(reader);209if (!activeDocument) { return; }210211const activeEditor = this._tabsAndEditorsService.activeTextEditor;212if (!activeEditor || !isEditorFromEditorGrid(activeEditor) || !isEqual(activeDocument.id.toUri(), activeEditor.document.uri)) {213return;214}215216// update state because document changed217this._updateState();218219// update state because diagnostics changed220reader.store.add(runOnChange(activeDocument.diagnostics, (diagnostics) => {221this._logger.trace(`Diagnostics changed received in processor: ${diagnostics.map(d => '\n- ' + d.message).join('')}`);222this._updateState();223}));224}));225226this._register(vscode.window.onDidChangeTextEditorSelection(async e => {227const activeEditor = this._tabsAndEditorsService.activeTextEditor;228if (!isValidEditor(activeEditor)) {229return;230}231232if (!isEqual(e.textEditor.document.uri, activeEditor.document.uri)) {233return;234}235236this._updateState();237}));238239this._register(this._worker.onDidChange(result => {240this._onDidChange.fire(!!result.completionItem);241}));242243this._register(autorun(reader => {244const document = this._workspace.lastActiveDocument.read(reader);245if (!document) { return; }246247reader.store.add(autorunWithChanges(this, {248value: document.value,249}, (data) => {250for (const edit of data.value.changes) {251if (!data.value.previous) { continue; }252const hasInvalidatedRange = this._currentDiagnostics.applyEdit(data.value.previous, edit, data.value.value);253if (hasInvalidatedRange) {254this._updateState();255}256}257}));258}));259}260261private async _updateState(): Promise<void> {262const activeTextEditor = this._tabsAndEditorsService.activeTextEditor;263if (!activeTextEditor) { return; }264265const workspaceDocument = this._workspace.getDocumentByTextDocument(activeTextEditor.document);266if (!workspaceDocument) { return; }267268const range = new vscode.Range(activeTextEditor.selection.active, activeTextEditor.selection.active);269const selection = workspaceDocument.toRange(activeTextEditor.document, range);270if (!selection) {271return;272}273274const cursor = toInternalPosition(selection.start);275const log = new DiagnosticInlineEditRequestLogContext();276277const relevantDiagnostics = this._getDiagnostics(workspaceDocument, cursor, log);278const diagnosticsSorted = sortDiagnosticsByDistance(workspaceDocument, relevantDiagnostics, cursor);279280if (this._currentDiagnostics.isEqualAndUpdate(diagnosticsSorted)) {281return;282}283284this._logger.trace('Scheduled update for diagnostics inline completion');285286await this._worker.schedule(async (token: CancellationToken) => this._runCompletionHandler(workspaceDocument, diagnosticsSorted, cursor, log, token));287}288289private _getDiagnostics(workspaceDocument: IVSCodeObservableDocument, cursor: Position, logContext: DiagnosticInlineEditRequestLogContext): Diagnostic[] {290const availableDiagnostics = workspaceDocument.diagnostics.get().map(d => new Diagnostic(d));291if (availableDiagnostics.length === 0) {292return [];293}294295const filterDiagnosticsAndLog = (diagnostics: Diagnostic[], message: string, filterFn: (diagnostics: Diagnostic[]) => Diagnostic[]): Diagnostic[] => {296const diagnosticsAfter = filterFn(diagnostics);297const diagnosticsDiff = diagnostics.filter(diagnostic => !diagnosticsAfter.includes(diagnostic));298if (diagnosticsDiff.length > 0) {299logList(message, diagnosticsDiff, logContext, this._logger);300}301return diagnosticsAfter;302};303304const language = workspaceDocument.languageId.get();305const providers = this._diagnosticsCompletionProviders.get();306307let relevantDiagnostics = [...availableDiagnostics];308relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by provider', ds => ds.filter(diagnostic => providers.some(provider => provider.providesCompletionsForDiagnostic(workspaceDocument, diagnostic, language, cursor))));309relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by recent acceptance', ds => ds.filter(diagnostic => !this._hasDiagnosticRecentlyBeenAccepted(diagnostic)));310relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by no recent edit', ds => this._filterDiagnosticsByRecentEditNearby(ds, workspaceDocument));311312return relevantDiagnostics;313}314315private async _runCompletionHandler(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], cursor: Position, log: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<IDiagnosticsCompletionState> {316const telemetryBuilder = new DiagnosticsCompletionHandlerTelemetry();317318let completionItem = null;319try {320this._logger.trace('Running diagnostics inline completion handler');321completionItem = await this._getCompletionFromDiagnostics(workspaceDocument, diagnosticsSorted, cursor, log, token, telemetryBuilder);322} catch (error) {323log.setError(error);324}325326this._logger.trace('Diagnostic Providers returned completion item: ' + (completionItem ? completionItem.toString() : 'null'));327328if (completionItem instanceof ImportDiagnosticCompletionItem) {329telemetryBuilder.setImportTelemetry(completionItem);330}331332return { completionItem, logContext: log, telemetryBuilder: telemetryBuilder };333}334335getCurrentState(docId: DocumentId): DiagnosticCompletionState {336const currentState = this._worker.getCurrentResult();337338const workspaceDocument = this._workspace.getDocument(docId);339if (!workspaceDocument) { return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().addDroppedReason('WorkspaceDocumentNotFound').build(), logContext: undefined }; }340341if (currentState === undefined) {342return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().build(), logContext: undefined };343}344345const { telemetryBuilder, completionItem, logContext } = currentState;346const workInProgress = this._worker.workInProgress();347if (!completionItem) {348return { item: undefined, telemetry: telemetryBuilder.build(), logContext, workInProgress };349}350351if (!this._isCompletionItemValid(completionItem, workspaceDocument, currentState.logContext, telemetryBuilder)) {352return { item: undefined, telemetry: telemetryBuilder.build(), logContext, workInProgress };353}354355if (completionItem.documentId !== docId) {356logContext.addLog('Dropped: wrong-document');357return { item: undefined, telemetry: telemetryBuilder.addDroppedReason('wrong-document').build(), logContext, workInProgress };358}359360log('following known diagnostics:\n' + this._currentDiagnostics.toString(), undefined, this._logger);361362return { item: completionItem, telemetry: telemetryBuilder.build(), logContext, workInProgress };363}364365private async _getCompletionFromDiagnostics(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken, tb: DiagnosticsCompletionHandlerTelemetry): Promise<DiagnosticCompletionItem | null> {366if (diagnosticsSorted.length === 0) {367log(`No diagnostics available for document ${workspaceDocument.id.toString()}`, logContext, this._logger);368return null;369}370371const diagnosticsCompletionItems = await this._fetchDiagnosticsBasedCompletions(workspaceDocument, diagnosticsSorted, pos, logContext, token);372373return diagnosticsCompletionItems.find(item => this._isCompletionItemValid(item, workspaceDocument, logContext, tb)) ?? null;374}375376private async _fetchDiagnosticsBasedCompletions(workspaceDocument: IVSCodeObservableDocument, sortedDiagnostics: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<DiagnosticCompletionItem[]> {377const providers = this._diagnosticsCompletionProviders.get();378379const providerTimings: Array<{ provider: string; duration: number }> = [];380381const providerResults = await Promise.all(providers.map(async provider => {382const startTime = Date.now();383const result = await provider.provideDiagnosticCompletionItem(workspaceDocument, sortedDiagnostics, pos, logContext, token);384providerTimings.push({ provider: provider.providerName, duration: Date.now() - startTime });385return result;386}));387388this._logger.trace(`Provider durations: ${providerTimings.map(timing => `\n- ${timing.provider}: ${timing.duration}ms`).join('')}`);389390return providerResults.filter(item => !!item) as DiagnosticCompletionItem[];391}392393// Handle Acceptance and rejection of diagnostics completion items394395public handleEndOfLifetime(completionItem: DiagnosticCompletionItem, reason: vscode.InlineCompletionEndOfLifeReason): void {396const provider = this._diagnosticsCompletionProviders.get().find(p => p.providerName === completionItem.providerName);397if (!provider) {398throw new BugIndicatingError('No provider found for completion item');399}400401if (reason.kind === vscode.InlineCompletionEndOfLifeReasonKind.Rejected) {402this._rejectDiagnosticCompletion(provider, completionItem);403} else if (reason.kind === vscode.InlineCompletionEndOfLifeReasonKind.Accepted) {404this._acceptDiagnosticCompletion(provider, completionItem);405}406}407408private _lastAcceptedDiagnostic: { diagnostic: Diagnostic; time: number } | undefined = undefined;409private _acceptDiagnosticCompletion(provider: IDiagnosticCompletionProvider, item: DiagnosticCompletionItem): void {410this._lastAcceptedDiagnostic = { diagnostic: item.diagnostic, time: Date.now() };411}412413private _rejectDiagnosticCompletion(provider: IDiagnosticCompletionProvider, item: DiagnosticCompletionItem): void {414this._rejectionCollector.reject(item.documentId, item.toOffsetEdit());415416provider.completionItemRejected?.(item);417}418419// Filters420421private _isCompletionItemValid(item: DiagnosticCompletionItem, workspaceDocument: IObservableDocument, logContext: DiagnosticInlineEditRequestLogContext, tb: DiagnosticsCompletionHandlerTelemetry): boolean {422if (!item.diagnostic.isValid()) {423log('Diagnostic completion item is no longer valid', logContext, this._logger);424tb.addDroppedReason('no-longer-valid', item);425logContext.markToBeLogged();426return false;427}428429if (this._isDiagnosticCompletionRejected(item)) {430log('Diagnostic completion item has been rejected before', logContext, this._logger);431tb.addDroppedReason('recently-rejected', item);432logContext.markToBeLogged();433return false;434}435436if (this._isUndoRecentEdit(item)) {437log('Diagnostic completion item is an undo operation', logContext, this._logger);438tb.addDroppedReason('undo-operation', item);439logContext.markToBeLogged();440return false;441}442443if (this._hasDiagnosticRecentlyBeenAccepted(item.diagnostic)) {444log('Completion item fixing the diagnostic has been accepted recently', logContext, this._logger);445tb.addDroppedReason('recently-accepted', item);446logContext.markToBeLogged();447return false;448}449450if (this._hasRecentlyBeenAddedWithoutNES(item)) {451log('Diagnostic has been fixed without NES recently', logContext, this._logger);452tb.addDroppedReason('recently-added-without-nes', item);453logContext.markToBeLogged();454return false;455}456457const provider = this._diagnosticsCompletionProviders.get().find(p => p.providerName === item.providerName);458if (provider && provider.isCompletionItemStillValid && !provider.isCompletionItemStillValid(item, workspaceDocument)) {459log(`${provider.providerName}: Completion item is no longer valid`, logContext, this._logger);460tb.addDroppedReason(`${provider.providerName}-no-longer-valid`, item);461logContext.markToBeLogged();462return false;463}464465return true;466}467468private _isDiagnosticCompletionRejected(diagnostic: DiagnosticCompletionItem): boolean {469return this._rejectionCollector.isRejected(diagnostic.documentId, diagnostic.toOffsetEdit());470}471472private _hasRecentlyBeenAddedWithoutNES(item: DiagnosticCompletionItem): boolean {473const recentEdits = this._workspaceDocumentEditHistory.getNRecentEdits(item.documentId, 5)?.edits;474if (!recentEdits) {475return false;476}477478const offsetEdit = item.toOffsetEdit();479return recentEdits.replacements.some(edit => edit.replaceRange.intersectsOrTouches(offsetEdit.replaceRange));480}481482private _hasDiagnosticRecentlyBeenAccepted(diagnostic: Diagnostic): boolean {483if (!this._lastAcceptedDiagnostic || this._lastAcceptedDiagnostic.time + 1000 < Date.now()) {484return false;485}486return this._lastAcceptedDiagnostic.diagnostic.equals(diagnostic);487}488489private _isUndoRecentEdit(diagnostic: DiagnosticCompletionItem): boolean {490const documentHistory = this._workspaceDocumentEditHistory.getRecentEdits(diagnostic.documentId);491if (!documentHistory) {492return false;493}494495return diagnosticWouldUndoUserEdit(diagnostic, documentHistory.before, documentHistory.after, Edits.single(documentHistory.edits));496}497498private _filterDiagnosticsByRecentEditNearby(diagnostics: Diagnostic[], document: IVSCodeObservableDocument): Diagnostic[] {499const recentEdits = this._workspaceDocumentEditHistory.getRecentEdits(document.id)?.edits;500if (!recentEdits) {501return [];502}503504return diagnostics.filter(diagnostic => {505const newRanges = recentEdits.getNewRanges();506const potentialIntersection = findFirstMonotonous(newRanges, (r) => r.endExclusive >= diagnostic.range.start);507return potentialIntersection?.intersectsOrTouches(diagnostic.range);508});509}510}511512function diagnosticWouldUndoUserEdit(diagnostic: DiagnosticCompletionItem, documentBefore: StringText, documentAfter: StringText, edits: Edits): boolean {513514const currentEdit = diagnostic.toOffsetEdit().toEdit();515const ourInformationDelta = getInformationDelta(documentAfter.value, currentEdit);516517let recentInformationDelta = new InformationDelta();518let doc = documentBefore.value;519for (const edit of edits.edits) {520recentInformationDelta = recentInformationDelta.combine(getInformationDelta(doc, edit));521doc = edit.apply(doc);522}523524if (recentInformationDelta.isUndoneBy(ourInformationDelta)) {525return true;526}527528return false;529}530531function isEditorFromEditorGrid(editor: vscode.TextEditor): boolean {532return editor.viewColumn !== undefined;533}534535class AsyncWorker<T extends {}> extends Disposable {536private readonly _taskQueue: ThrottledDelayer<void>;537538private readonly _onDidChange = this._register(new vscode.EventEmitter<T>());539readonly onDidChange = this._onDidChange.event;540541private _currentTokenSource: CancellationTokenSource | undefined = undefined;542private _activeWorkPromise: Promise<T | undefined> | undefined = undefined;543544private __currentResult: T | undefined = undefined;545private get _currentResult(): T | undefined {546return this.__currentResult;547}548private set _currentResult(value: T) {549const changed = this.__currentResult === undefined || !this._equals(value, this.__currentResult);550this.__currentResult = value;551if (changed) {552this._onDidChange.fire(value);553}554}555556constructor(delay: number, private readonly _equals: (a: T, b: T) => boolean) {557super();558559this._taskQueue = new ThrottledDelayer<void>(delay);560}561562async schedule(fn: (token: CancellationToken) => Promise<T>): Promise<void> {563const activePromise = this._doSchedule(fn);564this._activeWorkPromise = activePromise;565566const result = await activePromise;567568if (this._activeWorkPromise === activePromise) {569this._activeWorkPromise = undefined;570}571572if (result !== undefined) {573this._currentResult = result;574}575}576577private async _doSchedule(fn: (token: CancellationToken) => Promise<T>): Promise<T | undefined> {578this._currentTokenSource?.dispose(true);579this._currentTokenSource = new CancellationTokenSource();580const token = this._currentTokenSource.token;581582let result;583await this._taskQueue.trigger(async () => {584if (token.isCancellationRequested) {585return;586}587588result = await fn(token);589});590591return result;592}593594// Get the active result if there is one currently595// Return undefined if there is currently work being done596getCurrentResult(): T | undefined {597if (this._currentResult === undefined) {598return undefined;599}600601return this._currentResult;602}603604workInProgress(): boolean {605return this._activeWorkPromise !== undefined;606}607608override dispose(): void {609if (this._currentTokenSource) {610this._currentTokenSource.dispose();611}612super.dispose();613}614}615616interface IDiagnosticsCompletionTelemetry {617droppedReasons: string[];618alternativeImportsCount?: number;619hasExistingSameFileImport?: boolean;620isLocalImport?: boolean;621distanceToUnknownDiagnostic?: number;622distanceToAlternativeDiagnostic?: number;623hasAlternativeDiagnosticForSameRange?: boolean;624}625626class DiagnosticsCompletionHandlerTelemetry {627private _droppedReasons: string[] = [];628629addDroppedReason(reason: string, item?: DiagnosticCompletionItem): this {630if (item instanceof AnyDiagnosticCompletionItem) {631return this; // Do not track dropped reasons for "any" items632}633634this._droppedReasons.push(item ? `${item.type}:${reason}` : reason);635return this;636}637638private _distanceToAlternativeDiagnostic: number | undefined;639setDistanceToAlternativeDiagnostic(distance: number | undefined): this {640this._distanceToAlternativeDiagnostic = distance;641return this;642}643644private _distanceToUnknownDiagnostic: number | undefined;645setDistanceToUnknownDiagnostic(distance: number | undefined): this {646this._distanceToUnknownDiagnostic = distance;647return this;648}649650private _hasAlternativeDiagnosticForSameRange: boolean | undefined;651setHasAlternativeDiagnosticForSameRange(has: boolean | undefined): this {652this._hasAlternativeDiagnosticForSameRange = has;653return this;654}655656private _alternativeImportsCount: number | undefined;657private _hasExistingSameFileImport: boolean | undefined;658private _isLocalImport: boolean | undefined;659660setImportTelemetry(item: ImportDiagnosticCompletionItem): this {661this._alternativeImportsCount = item.alternativeImportsCount;662this._hasExistingSameFileImport = item.hasExistingSameFileImport;663this._isLocalImport = item.isLocalImport;664return this;665}666667build(): IDiagnosticsCompletionTelemetry {668return {669droppedReasons: this._droppedReasons,670alternativeImportsCount: this._alternativeImportsCount,671hasExistingSameFileImport: this._hasExistingSameFileImport,672isLocalImport: this._isLocalImport,673distanceToUnknownDiagnostic: this._distanceToUnknownDiagnostic,674distanceToAlternativeDiagnostic: this._distanceToAlternativeDiagnostic,675hasAlternativeDiagnosticForSameRange: this._hasAlternativeDiagnosticForSameRange676};677}678}679680681