Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsInlineEditProvider.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 type { Command } from 'vscode';6import * as vscode from 'vscode';7import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';8import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';9import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit';10import { ILogService, ILogger } from '../../../../platform/log/common/logService';11import { ErrorUtils } from '../../../../util/common/errors';12import { raceCancellation, timeout } from '../../../../util/vs/base/common/async';13import { CancellationToken } from '../../../../util/vs/base/common/cancellation';14import { BugIndicatingError } from '../../../../util/vs/base/common/errors';15import { Disposable, DisposableStore } from '../../../../util/vs/base/common/lifecycle';16import { StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';17import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';18import { INextEditProvider, NESInlineCompletionContext, NesOutcome } from '../../node/nextEditProvider';19import { DiagnosticsTelemetryBuilder } from '../../node/nextEditProviderTelemetry';20import { INextEditDisplayLocation, INextEditResult } from '../../node/nextEditResult';21import { VSCodeWorkspace } from '../parts/vscodeWorkspace';22import { DiagnosticCompletionItem } from './diagnosticsBasedCompletions/diagnosticsCompletions';23import { DiagnosticCompletionState, DiagnosticsCompletionProcessor } from './diagnosticsCompletionProcessor';2425export class DiagnosticsNextEditResult implements INextEditResult {26constructor(27public readonly requestId: number,28public readonly result: {29edit: StringReplacement;30displayLocation?: INextEditDisplayLocation;31item: DiagnosticCompletionItem;32action?: Command;33} | undefined,34public workInProgress: boolean = false35) { }36}3738export class DiagnosticsNextEditProvider extends Disposable implements INextEditProvider<DiagnosticsNextEditResult, DiagnosticsTelemetryBuilder, boolean> {39public readonly ID = 'DiagnosticsNextEditProvider';4041private _lastRejectionTime: number = 0;42public get lastRejectionTime(): number {43return this._lastRejectionTime;44}4546private _lastTriggerTime: number = 0;47public get lastTriggerTime(): number {48return this._lastTriggerTime;49}5051private _lastOutcome: NesOutcome | undefined;52public get lastOutcome(): NesOutcome | undefined {53return this._lastOutcome;54}5556private readonly _diagnosticsCompletionHandler: DiagnosticsCompletionProcessor;57private _logger: ILogger;5859constructor(60workspace: VSCodeWorkspace,61git: ObservableGit,62@IInstantiationService instantiationService: IInstantiationService,63@ILogService logService: ILogService,64) {65super();6667this._logger = logService.createSubLogger(['NES', 'DiagnosticsNextEditProvider']);68this._diagnosticsCompletionHandler = this._register(instantiationService.createInstance(DiagnosticsCompletionProcessor, workspace, git));69}7071async getNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise<DiagnosticsNextEditResult> {72this._lastTriggerTime = Date.now();73throw new BugIndicatingError('DiagnosticsNextEditProvider does not support getNextEdit, use runUntilNextEdit instead');74}7576async runUntilNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, delayStart: number, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise<DiagnosticsNextEditResult> {77try {78await timeout(delayStart);79if (cancellationToken.isCancellationRequested) {80this._logger.trace('cancellationRequested before started');81return new DiagnosticsNextEditResult(logContext.requestId, undefined);82}8384// Check if the last computed edit is still valid85const initialResult = this._getResultForCurrentState(docId, logContext, tb);86if (initialResult.result) {87return initialResult;88}8990const asyncResult = await raceCancellation(new Promise<DiagnosticsNextEditResult>((resolve) => {91const disposables = new DisposableStore();92const complete = (result: DiagnosticsNextEditResult) => {93resolve(result);94disposables.dispose();95};9697disposables.add(this._diagnosticsCompletionHandler.onDidChange(() => {98const completionResult = this._getResultForCurrentState(docId, logContext, tb);99if (completionResult.result || !completionResult.workInProgress) {100complete(completionResult);101}102}));103104disposables.add(cancellationToken.onCancellationRequested(() => {105disposables.dispose();106}));107}), cancellationToken);108109return asyncResult ?? initialResult;110} catch (error) {111const errorMessage = `Error occurred while waiting for diagnostic edit: ${ErrorUtils.toString(ErrorUtils.fromUnknown(error))}`;112logContext.addLog(errorMessage);113this._logger.trace(errorMessage);114return new DiagnosticsNextEditResult(logContext.requestId, undefined);115} finally {116this._logger.trace('DiagnosticsInlineCompletionProvider runUntilNextEdit complete' + (cancellationToken.isCancellationRequested ? ' (cancelled)' : ''));117}118}119120private _getResultForCurrentState(docId: DocumentId, logContext: InlineEditRequestLogContext, tb: DiagnosticsTelemetryBuilder): DiagnosticsNextEditResult {121const completionResult = this._diagnosticsCompletionHandler.getCurrentState(docId);122const telemetry = new DiagnosticsTelemetryBuilder();123const diagnosticEditResult = this._createNextEditResult(completionResult, logContext, telemetry);124if (diagnosticEditResult.result) {125telemetry.populate(tb);126}127return diagnosticEditResult;128}129130private _createNextEditResult(diagnosticEditResult: DiagnosticCompletionState, logContext: InlineEditRequestLogContext, tb: DiagnosticsTelemetryBuilder): DiagnosticsNextEditResult {131const { item, telemetry } = diagnosticEditResult;132133// Diagnostics might not have updated yet since accepting a diagnostics based NES134if (item && this._hasRecentlyBeenAccepted(item)) {135tb.addDroppedReason(`${item.type}:recently-accepted`);136this._logger.trace('recently accepted');137return new DiagnosticsNextEditResult(logContext.requestId, undefined, diagnosticEditResult.workInProgress);138}139140telemetry.droppedReasons.forEach(reason => tb.addDroppedReason(reason));141tb.setDiagnosticRunTelemetry(telemetry);142143if (!item) {144this._logger.trace('no diagnostic edit result');145return new DiagnosticsNextEditResult(logContext.requestId, undefined, diagnosticEditResult.workInProgress);146}147148tb.setType(item.type);149logContext.setDiagnosticsResult(item.getRootedLineEdit());150151this._logger.trace(`created next edit result`);152153return new DiagnosticsNextEditResult(logContext.requestId, {154edit: item.toOffsetEdit(),155displayLocation: item.nextEditDisplayLocation,156item157}, diagnosticEditResult.workInProgress);158}159160handleShown(suggestion: DiagnosticsNextEditResult): void { }161162handleAcceptance(docId: DocumentId, suggestion: DiagnosticsNextEditResult): void {163const completionResult = suggestion.result;164if (!completionResult) {165throw new BugIndicatingError('Completion result is undefined when accepted');166}167168this._lastAcceptedItem = { item: completionResult.item, time: Date.now() };169this._lastOutcome = NesOutcome.Accepted;170this._diagnosticsCompletionHandler.handleEndOfLifetime(completionResult.item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Accepted });171}172173private _lastAcceptedItem: { item: DiagnosticCompletionItem; time: number } | undefined = undefined;174private _hasRecentlyBeenAccepted(item: DiagnosticCompletionItem): boolean {175if (!this._lastAcceptedItem) {176return false;177}178179if (Date.now() - this._lastAcceptedItem.time >= 1000) {180return false;181}182183return item.diagnostic.equals(this._lastAcceptedItem.item.diagnostic) || DiagnosticCompletionItem.equals(this._lastAcceptedItem.item, item);184}185186handleRejection(docId: DocumentId, suggestion: DiagnosticsNextEditResult): void {187this._lastRejectionTime = Date.now();188this._lastOutcome = NesOutcome.Rejected;189190const completionResult = suggestion.result;191if (!completionResult) {192throw new BugIndicatingError('Completion result is undefined when rejected');193}194195this._diagnosticsCompletionHandler.handleEndOfLifetime(completionResult.item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Rejected });196}197198handleIgnored(docId: DocumentId, suggestion: DiagnosticsNextEditResult, supersededBy: INextEditResult | undefined): void {199this._lastOutcome = NesOutcome.Ignored;200201const completionResult = suggestion.result;202if (!completionResult) {203throw new BugIndicatingError('Completion result is undefined when accepted');204}205206const supersededByItem = supersededBy instanceof DiagnosticsNextEditResult ? supersededBy?.result?.item : undefined;207208this._diagnosticsCompletionHandler.handleEndOfLifetime(completionResult.item, {209kind: vscode.InlineCompletionEndOfLifeReasonKind.Ignored,210supersededBy: supersededByItem,211userTypingDisagreed: false /* TODO: Adopt this*/212});213}214215}216217218