Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts
13399 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 l10n from '@vscode/l10n';6import { CancellationToken, Command, EndOfLine, InlineCompletionContext, InlineCompletionDisplayLocation, InlineCompletionDisplayLocationKind, InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletionItem, InlineCompletionItemProvider, InlineCompletionList, InlineCompletionModelInfo, InlineCompletionProviderOption, InlineCompletionsDisposeReason, InlineCompletionsDisposeReasonKind, NotebookCell, NotebookCellKind, Position, Range, TextDocument, TextDocumentShowOptions, Uri, window, workspace } from 'vscode';7import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';8import { IDiffService } from '../../../platform/diff/common/diffService';9import { stringEditFromDiff } from '../../../platform/editing/common/edit';10import { DocumentEditRecorder } from '../../../platform/editSurvivalTracking/common/editComputer';11import { EditSurvivalReporter } from '../../../platform/editSurvivalTracking/common/editSurvivalReporter';12import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';13import { DocumentId } from '../../../platform/inlineEdits/common/dataTypes/documentId';14import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext';15import { IInlineEditsModelService } from '../../../platform/inlineEdits/common/inlineEditsModelService';16import { shortenOpportunityId } from '../../../platform/inlineEdits/common/utils/utils';17import { ILogger, ILogService } from '../../../platform/log/common/logService';18import { getNotebookId } from '../../../platform/notebook/common/helpers';19import { INotebookService } from '../../../platform/notebook/common/notebookService';20import { CapturingToken } from '../../../platform/requestLogger/common/capturingToken';21import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';22import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';23import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';24import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';25import { findCell, findNotebook, isNotebookCell } from '../../../util/common/notebooks';26import { assert } from '../../../util/vs/base/common/assert';27import { raceCancellation, timeout } from '../../../util/vs/base/common/async';28import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation';29import { BugIndicatingError, onUnexpectedError } from '../../../util/vs/base/common/errors';30import { Emitter } from '../../../util/vs/base/common/event';31import { Disposable } from '../../../util/vs/base/common/lifecycle';32import { clamp } from '../../../util/vs/base/common/numbers';33import { autorun, IObservable, observableFromEvent } from '../../../util/vs/base/common/observable';34import { basename } from '../../../util/vs/base/common/path';35import { StringEdit } from '../../../util/vs/editor/common/core/edits/stringEdit';36import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';37import { createCorrelationId } from '../common/correlationId';38import { NesChangeHint } from '../common/nesTriggerHint';39import { NESInlineCompletionContext } from '../node/nextEditProvider';40import { NextEditProviderTelemetryBuilder, TelemetrySender } from '../node/nextEditProviderTelemetry';41import { INextEditResult, NextEditResult } from '../node/nextEditResult';42import { ExpectedEditCaptureController } from './components/expectedEditCaptureController';43import { InlineCompletionCommand, InlineEditDebugComponent } from './components/inlineEditDebugComponent';44import { LogContextRecorder } from './components/logContextRecorder';45import { DiagnosticsNextEditResult } from './features/diagnosticsInlineEditProvider';46import { InlineEditModel } from './inlineEditModel';47import { learnMoreCommandId, learnMoreLink } from './inlineEditProviderFeature';48import { toInlineSuggestion } from './isInlineSuggestion';49import { LineCheck } from './naturalLanguageHint';50import { InlineEditLogger } from './parts/inlineEditLogger';51import { IVSCodeObservableDocument } from './parts/vscodeWorkspace';52import { raceAndAll } from './raceAndAll';53import { toExternalRange } from './utils/translations';5455const learnMoreAction: Command = {56title: l10n.t('Learn More'),57command: learnMoreCommandId,58tooltip: learnMoreLink59};6061export interface NesCompletionItem extends InlineCompletionItem {62readonly telemetryBuilder: NextEditProviderTelemetryBuilder;63readonly info: NesCompletionInfo;64wasShown: boolean;65isEditInAnotherDocument?: boolean;66/**67* Whether the underlying NES suggestion is being served as an inline (ghost68* text at cursor) suggestion as opposed to a non-inline NES (e.g. gutter or69* side hint). Used by the "mimic ghost text behavior" gating.70*/71isInlineCompletion?: boolean;72}7374export class NesCompletionList extends InlineCompletionList {7576public override enableForwardStability = true;7778constructor(79public readonly requestUuid: string,80item: NesCompletionItem | undefined,81public override readonly commands: InlineCompletionCommand[],82public readonly telemetryBuilder: NextEditProviderTelemetryBuilder,83) {84super(item === undefined ? [] : [item]);85}86}8788abstract class BaseNesCompletionInfo<T extends INextEditResult> {8990public abstract source: string;9192constructor(93public readonly suggestion: T,94public readonly documentId: DocumentId,95public readonly document: TextDocument,96public readonly requestUuid: string97) { }98}99100class LlmCompletionInfo extends BaseNesCompletionInfo<NextEditResult> {101public readonly source = 'provider';102}103104class DiagnosticsCompletionInfo extends BaseNesCompletionInfo<DiagnosticsNextEditResult> {105public readonly source = 'diagnostics';106}107108type NesCompletionInfo = LlmCompletionInfo | DiagnosticsCompletionInfo;109110function isLlmCompletionInfo(item: NesCompletionInfo): item is LlmCompletionInfo {111return item.source === 'provider';112}113114const GoToNextEdit = l10n.t('Go To Inline Suggestion');115116117export class InlineCompletionProviderImpl extends Disposable implements InlineCompletionItemProvider {118public readonly displayName = 'Inline Suggestion';119120private readonly _logger: ILogger;121122public readonly onDidChange = this.model.onChange;123public readonly handleDidPartiallyAcceptCompletionItem = undefined;124public readonly handleDidRejectCompletionItem = undefined;125126//#region Model picker127private _isModelPickerEnabled: IObservable<boolean> = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsModelPickerEnabled, this._expService);128129public modelInfo: InlineCompletionModelInfo | undefined;130131private readonly _onDidChangeModelInfo = this._register(new Emitter<void>());132public onDidChangeModelInfo = this._onDidChangeModelInfo.event;133134public setCurrentModelId: ((modelId: string) => Thenable<void>) | undefined;135//#endregion136137//#region Provider options (Eagerness)138private static readonly _aggressivenessOptionId = 'eagerness';139140providerOptions: readonly InlineCompletionProviderOption[] | undefined;141142private readonly _onDidChangeProviderOptions = this._register(new Emitter<void>());143readonly onDidChangeProviderOptions = this._onDidChangeProviderOptions.event;144145setProviderOptionValue = async (optionId: string, valueId: string): Promise<void> => {146if (optionId === InlineCompletionProviderImpl._aggressivenessOptionId) {147await this._configurationService.setConfig(ConfigKey.Advanced.InlineEditsAggressiveness, valueId);148}149};150//#endregion151152private readonly _displayNextEditorNES: boolean;153private readonly _renameSymbolSuggestions: IObservable<boolean>;154private readonly _inlineCompletionsAdvanced: IObservable<boolean>;155private readonly _nesMimicGhostTextBehavior: IObservable<boolean>;156157constructor(158private readonly model: InlineEditModel,159private readonly logger: InlineEditLogger,160private readonly logContextRecorder: LogContextRecorder | undefined,161private readonly inlineEditDebugComponent: InlineEditDebugComponent | undefined,162private readonly telemetrySender: TelemetrySender,163private readonly expectedEditCaptureController: ExpectedEditCaptureController,164@IInstantiationService private readonly _instantiationService: IInstantiationService,165@ITelemetryService private readonly _telemetryService: ITelemetryService,166@IDiffService private readonly _diffService: IDiffService,167@IConfigurationService private readonly _configurationService: IConfigurationService,168@ILogService private readonly _logService: ILogService,169@IExperimentationService private readonly _expService: IExperimentationService,170@IGitExtensionService private readonly _gitExtensionService: IGitExtensionService,171@INotebookService private readonly _notebookService: INotebookService,172@IWorkspaceService private readonly _workspaceService: IWorkspaceService,173@IRequestLogger private readonly _requestLogger: IRequestLogger,174@IInlineEditsModelService private readonly _modelService: IInlineEditsModelService,175) {176super();177this._logger = this._logService.createSubLogger(['NES', 'Provider']);178this._displayNextEditorNES = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.UseAlternativeNESNotebookFormat, this._expService);179this._renameSymbolSuggestions = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.Advanced.InlineEditsRenameSymbolSuggestions, this._expService);180this._inlineCompletionsAdvanced = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsInlineCompletionsAdvanced, this._expService);181this._nesMimicGhostTextBehavior = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsNesMimicGhostTextBehavior, this._expService);182183this.setCurrentModelId = (modelId: string) => this._modelService.setCurrentModelId(modelId);184185const modelListUpdatedObs = observableFromEvent(this, this._modelService.onModelListUpdated, () => this._modelService.modelInfo);186187this._register(autorun(reader => {188this.modelInfo = this._isModelPickerEnabled.read(reader) ? modelListUpdatedObs.read(reader) : undefined;189this._onDidChangeModelInfo.fire();190}));191192// Provider options: eagerness193const aggressivenessObs = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.Advanced.InlineEditsAggressiveness, this._expService);194195this._register(autorun(reader => {196const current = aggressivenessObs.read(reader);197this.providerOptions = [{198id: InlineCompletionProviderImpl._aggressivenessOptionId,199label: l10n.t('Eagerness'),200values: [201{ id: 'auto', label: l10n.t('Auto') },202{ id: 'low', label: l10n.t('Low') },203{ id: 'medium', label: l10n.t('Medium') },204{ id: 'high', label: l10n.t('High') },205],206currentValueId: current,207}];208this._onDidChangeProviderOptions.fire();209}));210211}212213// copied from `vscodeWorkspace.ts` `DocumentFilter#_enabledLanguages`214private _isCompletionsEnabled(document: TextDocument): boolean {215const enabledLanguages = this._configurationService.getConfig(ConfigKey.Enable);216const enabledLanguagesMap = new Map(Object.entries(enabledLanguages));217if (!enabledLanguagesMap.has('*')) {218enabledLanguagesMap.set('*', false);219}220return enabledLanguagesMap.has(document.languageId) ? enabledLanguagesMap.get(document.languageId)! : enabledLanguagesMap.get('*')!;221}222223public async provideInlineCompletionItems(224document: TextDocument,225position: Position,226context: InlineCompletionContext | NESInlineCompletionContext,227token: CancellationToken228): Promise<NesCompletionList | undefined> {229const label = `NES | ${basename(document.uri.fsPath)} (v${document.version})`;230231const capturingToken = new CapturingToken(label, undefined);232233assert(context.changeHint === undefined || NesChangeHint.is(context.changeHint), 'Expected changeHint to be of type TriggerNes or undefined');234const changeHint = context.changeHint as NesChangeHint | undefined;235const nesContext: NESInlineCompletionContext = { enforceCacheDelay: true, ...context, changeHint };236237return this._requestLogger.captureInvocation(capturingToken, () => this._provideInlineCompletionItems(document, position, nesContext, token));238}239240private async _provideInlineCompletionItems(241document: TextDocument,242position: Position,243context: NESInlineCompletionContext,244token: CancellationToken245): Promise<NesCompletionList | undefined> {246const logger = this._logger.createSubLogger(['provideInlineCompletionItems', shortenOpportunityId(context.requestUuid)]);247248// Disable NES while capture mode is active to avoid interference249if (this.expectedEditCaptureController.isCaptureActive) {250logger.trace('Return: capture mode active');251return undefined;252}253254const isCompletionsEnabled = this._isCompletionsEnabled(document);255256const unification = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsUnification, this._expService);257258const isInlineEditsEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.InlineEditsEnabled, this._expService, { languageId: document.languageId });259260const serveAsCompletionsProvider = unification && isCompletionsEnabled && !isInlineEditsEnabled;261262if (!isInlineEditsEnabled && !serveAsCompletionsProvider) {263logger.trace('Return: inline edits disabled');264return undefined;265}266267const ignoreWhenSuggestVisible = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsIgnoreWhenSuggestVisible, this._expService);268269if (ignoreWhenSuggestVisible && context.selectedCompletionInfo && !unification) {270logger.trace('Return: suggest widget is showing, not providing NES');271return undefined;272}273274const doc = this.model.workspace.getDocumentByTextDocument(document);275if (!doc) {276logger.trace('Return: document not found in workspace');277return undefined;278}279280const documentVersion = (isNotebookCell(document.uri) ? findNotebook(document.uri, workspace.notebookDocuments)?.version : undefined) || document.version;281const logContext = new InlineEditRequestLogContext(doc.id.uri, documentVersion, context);282logContext.recordingBookmark = this.model.debugRecorder.createBookmark();283this.logger.addLive(logContext);284285const telemetryBuilder = new NextEditProviderTelemetryBuilder(this._gitExtensionService, this._notebookService, this._workspaceService, this.model.nextEditProvider.ID, doc, this.model.debugRecorder, logContext.recordingBookmark);286telemetryBuilder.setOpportunityId(context.requestUuid);287telemetryBuilder.setConfigIsDiagnosticsNESEnabled(!!this.model.diagnosticsBasedProvider);288telemetryBuilder.setIsNaturalLanguageDominated(LineCheck.isNaturalLanguageDominated(document, position));289290const requestCancellationTokenSource = new CancellationTokenSource(token);291let suggestionInfo: NesCompletionInfo | undefined;292try {293logger.trace('invoking next edit provider');294295const { first, all } = raceAndAll([296this.model.nextEditProvider.getNextEdit(doc.id, context, logContext, token, telemetryBuilder.nesBuilder).then(r => ({ kind: 'llm' as const, val: r })),297(this.model.diagnosticsBasedProvider?.runUntilNextEdit(doc.id, context, logContext, 50, requestCancellationTokenSource.token, telemetryBuilder.diagnosticsBuilder)298?? raceCancellation(new Promise<undefined>(() => { }), requestCancellationTokenSource.token)).then(r => ({ kind: 'diagnostics' as const, val: r }))299], onUnexpectedError);300301const [llmSuggestion, diagnosticsSuggestion] = await first;302303let suggestion: {304kind: 'llm';305val: NextEditResult;306} | {307kind: 'diagnostics';308val: DiagnosticsNextEditResult | undefined;309};310311if (llmSuggestion !== undefined) {312if (llmSuggestion.val.result !== undefined || this.model.diagnosticsBasedProvider === undefined) {313suggestion = llmSuggestion;314} else {315logger.trace('giving some more time to diagnostics provider');316const remainingTime = clamp(1250 - (Date.now() - context.requestIssuedDateTime), 0, 1250);317timeout(remainingTime).then(() => requestCancellationTokenSource.cancel());318[, suggestion] = await all;319}320} else if (diagnosticsSuggestion !== undefined) {321if (diagnosticsSuggestion.val !== undefined && diagnosticsSuggestion.val.result !== undefined) {322suggestion = diagnosticsSuggestion;323} else {324[suggestion] = await all;325}326} else {327throw new BugIndicatingError('At least one of LLM NES or Diagnostics NES must be defined');328}329330// Cancel ongoing requests331requestCancellationTokenSource.cancel();332333const emptyList = new NesCompletionList(context.requestUuid, undefined, [], telemetryBuilder);334const isFromCursorJump = suggestion.kind === 'llm' && (335// edit came using cursor jump336!!(suggestion.val.result?.isFromCursorJump) ||337// no edit but cursor jump suggested jumping to a certain position338!!(suggestion.val.result?.jumpToPosition)339);340const correlationId = createCorrelationId('nes', { isFromCursorJump });341342if (token.isCancellationRequested) {343logger.trace('Return: lost race to cancellation');344this.telemetrySender.scheduleSendingEnhancedTelemetry({ requestId: logContext.requestId, result: undefined }, telemetryBuilder);345return emptyList;346}347348// Determine which suggestion to use349if (suggestion.kind === 'diagnostics' && suggestion.val && suggestion.val.result) {350suggestionInfo = new DiagnosticsCompletionInfo(suggestion.val, doc.id, document, context.requestUuid);351} else if (suggestion.kind === 'llm') {352suggestionInfo = new LlmCompletionInfo(suggestion.val, doc.id, document, context.requestUuid);353} else {354this.telemetrySender.scheduleSendingEnhancedTelemetry({ requestId: logContext.requestId, result: undefined }, telemetryBuilder);355return emptyList;356}357358if (suggestionInfo.source === 'provider' && suggestionInfo.suggestion.result?.jumpToPosition !== undefined) {359logger.trace('next edit suggestion only has jumpToPosition');360this.telemetrySender.scheduleSendingEnhancedTelemetry(suggestionInfo.suggestion, telemetryBuilder);361const positionToJumpOneBased = suggestionInfo.suggestion.result.jumpToPosition;362const jumpToPosition = new Position(positionToJumpOneBased.lineNumber - 1, positionToJumpOneBased.column - 1);363const targetDocumentId = suggestionInfo.suggestion.result.targetDocumentId;364telemetryBuilder.setIsNESForOtherEditor(!!targetDocumentId && targetDocumentId !== doc.id);365const jumpToPositionCompletionItem: NesCompletionItem = {366insertText: undefined as unknown as string,367info: suggestionInfo,368wasShown: false,369telemetryBuilder,370jumpToPosition,371...(targetDocumentId ? { uri: Uri.parse(targetDocumentId.uri) } : {}),372correlationId373};374return new NesCompletionList(context.requestUuid, jumpToPositionCompletionItem, [], telemetryBuilder);375}376377// Return and send telemetry if there is no result378const result = suggestionInfo.suggestion.result;379if (!result || !result.edit) {380logger.trace('no next edit suggestion');381this.telemetrySender.scheduleSendingEnhancedTelemetry(suggestionInfo.suggestion, telemetryBuilder);382return emptyList;383}384385logger.trace(`using next edit suggestion from ${suggestionInfo.source}`);386let isInlineCompletion: boolean = false;387let completionItem: Omit<NesCompletionItem, 'telemetryBuilder' | 'info' | 'showInlineEditMenu' | 'action' | 'wasShown' | 'isInlineEdit'> | undefined;388389// When the edit targets a different document, resolve the range against the target document390const targetDocumentId = isLlmCompletionInfo(suggestionInfo) ? suggestionInfo.suggestion.result?.targetDocumentId : undefined;391let resolveDoc = doc;392if (targetDocumentId && targetDocumentId !== doc.id) {393const targetTextDoc = this._workspaceService.textDocuments.find(d => d.uri.toString() === targetDocumentId.uri);394const targetObsDoc = targetTextDoc ? this.model.workspace.getDocumentByTextDocument(targetTextDoc) : undefined;395if (targetObsDoc) {396resolveDoc = targetObsDoc;397} else {398logger.trace('no next edit suggestion: cross-file target document not found in workspace');399telemetryBuilder.setIsNESForOtherEditor(true);400telemetryBuilder.setStatus('noEdit:crossFileTargetNotFound');401this.telemetrySender.scheduleSendingEnhancedTelemetry(suggestionInfo.suggestion, telemetryBuilder);402return emptyList;403}404}405const documents = resolveDoc.fromOffsetRange(result.edit.replaceRange);406const [targetDocument, range] = documents.length ? documents[0] : [undefined, undefined];407408addNotebookTelemetry(document, position, result.edit.newText, documents, telemetryBuilder);409telemetryBuilder.setIsNESForOtherEditor(targetDocument !== undefined && targetDocument !== document);410telemetryBuilder.setIsActiveDocument(window.activeTextEditor?.document === targetDocument);411412if (!targetDocument) {413logger.trace('no next edit suggestion');414} else if (hasNotebookCellMarker(document, result.edit.newText)) {415logger.trace('no next edit suggestion, edits contain Notebook Cell Markers');416} else if (isNotebookCell(targetDocument.uri) && this._displayNextEditorNES && targetDocument !== document) {417// NES is for a different notebook cell418completionItem = serveAsCompletionsProvider ?419undefined :420this.createNextEditorEditCompletionItem(position, {421document: targetDocument,422insertText: result.edit.newText,423range424});425} else if (targetDocument === document) { // NES is for the active document426const allowInlineCompletions = this.model.inlineEditsInlineCompletionsEnabled.get();427const inlineSuggestion = allowInlineCompletions ? toInlineSuggestion(position, document, range, result.edit.newText, this._inlineCompletionsAdvanced.get()) : undefined;428isInlineCompletion = !!inlineSuggestion;429completionItem = serveAsCompletionsProvider && !isInlineCompletion ?430undefined :431this.createCompletionItem(doc, document, inlineSuggestion?.range ?? range, result, inlineSuggestion?.newText);432} else { // NES is not for the active doc but a different one433completionItem = serveAsCompletionsProvider ? undefined : {434range,435insertText: result.edit.newText,436command: result.action,437uri: targetDocument.uri,438};439}440441// Gate: when the "mimic ghost text behavior" setting is on, a cached suggestion442// that was previously rendered as an inline (ghost text) suggestion must not443// re-surface in any other form. Suppress here without evicting the cache entry —444// when the cursor returns to an inline-renderable position, we'll serve it again.445if (446this._nesMimicGhostTextBehavior.get()447&& !isInlineCompletion448&& isLlmCompletionInfo(suggestionInfo)449&& suggestionInfo.suggestion.result?.cacheEntry?.wasRenderedAsInlineSuggestion450) {451logger.trace('Return: previously shown as inline; current context cannot render as inline');452telemetryBuilder.setStatus('noEdit:suppressedNonInlineReshow');453this.telemetrySender.scheduleSendingEnhancedTelemetry(suggestionInfo.suggestion, telemetryBuilder);454return emptyList;455}456457if (!completionItem) {458this.telemetrySender.scheduleSendingEnhancedTelemetry(suggestionInfo.suggestion, telemetryBuilder);459return emptyList;460}461462const menuCommands: InlineCompletionCommand[] = [];463if (this.inlineEditDebugComponent) {464menuCommands.push(...this.inlineEditDebugComponent.getCommands(logContext));465}466467468// telemetry469telemetryBuilder.setPickedNESType(suggestionInfo.source === 'diagnostics' ? 'diagnostics' : 'llm');470logContext.setPickedNESType(suggestionInfo.source === 'diagnostics' ? 'diagnostics' : 'llm');471telemetryBuilder.setPostProcessingOutcome({ edit: result.edit, displayLocation: result.displayLocation, isInlineCompletion });472telemetryBuilder.setHadLlmNES(suggestionInfo.source === 'provider');473telemetryBuilder.setHadDiagnosticsNES(suggestionInfo.source === 'diagnostics');474all.then(([llmResult, diagnosticsResult]) => {475telemetryBuilder.setHadLlmNES(!!llmResult?.val);476telemetryBuilder.setHadDiagnosticsNES(!!diagnosticsResult?.val);477});478479this.telemetrySender.scheduleSendingEnhancedTelemetry(suggestionInfo.suggestion, telemetryBuilder);480481const supportsRename = (document.languageId === 'typescript' || document.languageId === 'typescriptreact') && this._renameSymbolSuggestions.get();482483const nesCompletionItem: NesCompletionItem = {484...completionItem,485info: suggestionInfo,486telemetryBuilder,487action: learnMoreAction,488isInlineEdit: !isInlineCompletion,489isInlineCompletion,490showInlineEditMenu: !(unification && isInlineCompletion),491wasShown: false,492supportsRename,493correlationId,494};495496return new NesCompletionList(context.requestUuid, nesCompletionItem, menuCommands, telemetryBuilder);497} catch (e) {498logger.trace(`error: ${e}`);499logContext.setError(e);500501try {502this.telemetrySender.sendTelemetry(suggestionInfo?.suggestion, telemetryBuilder);503} finally {504telemetryBuilder.dispose();505}506507throw e;508} finally {509logContext.markCompleted();510requestCancellationTokenSource.dispose();511}512}513514private createNextEditorEditCompletionItem(requestingPosition: Position,515nextEdit: { document: TextDocument; range: Range; insertText: string }516): Omit<NesCompletionItem, 'telemetryBuilder' | 'info' | 'showInlineEditMenu' | 'action' | 'wasShown' | 'isInlineEdit'> {517// Display the next edit in the current document, but with a command to open the next edit in the other document.518// & range of this completion item will be the same as the current documents cursor position.519const range = new Range(requestingPosition, requestingPosition);520const displayLocation: InlineCompletionDisplayLocation = {521range,522label: GoToNextEdit,523kind: InlineCompletionDisplayLocationKind.Label524};525526const commandArgs: TextDocumentShowOptions = {527preserveFocus: false,528selection: new Range(nextEdit.range.start, nextEdit.range.start)529};530const command: Command = {531command: 'vscode.open',532title: GoToNextEdit,533arguments: [nextEdit.document.uri, commandArgs]534};535return {536range,537insertText: nextEdit.insertText,538showRange: range,539command,540displayLocation,541isEditInAnotherDocument: true542};543}544545private createCompletionItem(546doc: IVSCodeObservableDocument,547document: TextDocument,548range: Range,549result: NonNullable<(NextEditResult | DiagnosticsNextEditResult)['result']>,550insertTextOverride?: string,551): Omit<NesCompletionItem, 'telemetryBuilder' | 'info' | 'showInlineEditMenu' | 'action' | 'wasShown' | 'isInlineEdit'> | undefined {552553if (!result.edit) {554return undefined;555}556557const displayLocationRange = result.displayLocation && doc.fromRange(document, toExternalRange(result.displayLocation.range));558const displayLocation: InlineCompletionDisplayLocation | undefined = result.displayLocation && displayLocationRange ? {559range: displayLocationRange,560label: result.displayLocation.label,561kind: InlineCompletionDisplayLocationKind.Code,562} : undefined;563564565return {566range,567insertText: insertTextOverride ?? result.edit.newText,568displayLocation,569command: result.action,570};571}572573public handleDidShowCompletionItem(completionItem: NesCompletionItem, _updatedInsertText: string): void {574completionItem.wasShown = true;575completionItem.telemetryBuilder.setAsShown();576577const info = completionItem.info;578this.logContextRecorder?.handleShown(info.suggestion);579580if (isLlmCompletionInfo(info)) {581// Mark the underlying cache entry as having been rendered as an inline582// (ghost text) suggestion. The "mimic ghost text behavior" gate uses this583// flag to suppress re-serving the same suggestion in a non-inline form.584if (completionItem.isInlineCompletion) {585const cacheEntry = info.suggestion.result?.cacheEntry;586if (cacheEntry) {587cacheEntry.wasRenderedAsInlineSuggestion = true;588}589}590this.model.nextEditProvider.handleShown(info.suggestion);591} else {592this.model.diagnosticsBasedProvider?.handleShown(info.suggestion);593}594}595596public handleListEndOfLifetime(list: NesCompletionList, reason: InlineCompletionsDisposeReason): void {597const logger = this._logger.createSubLogger(['handleListEndOfLifetime', shortenOpportunityId(list.requestUuid)]);598logger.trace(`List ${list.requestUuid} disposed, reason: ${InlineCompletionsDisposeReasonKind[reason.kind]}`);599600const telemetryBuilder = list.telemetryBuilder;601602const disposeReasonStr = InlineCompletionsDisposeReasonKind[reason.kind];603604telemetryBuilder.setDisposalReason(disposeReasonStr);605606this.telemetrySender.sendTelemetryForBuilder(telemetryBuilder);607}608609public handleEndOfLifetime(item: NesCompletionItem, reason: InlineCompletionEndOfLifeReason): void {610const logger = this._logger.createSubLogger(['handleEndOfLifetime', shortenOpportunityId(item.info.requestUuid)]);611logger.trace(`reason: ${InlineCompletionEndOfLifeReasonKind[reason.kind]}`);612613switch (reason.kind) {614case InlineCompletionEndOfLifeReasonKind.Accepted: {615this._handleAcceptance(item);616break;617}618case InlineCompletionEndOfLifeReasonKind.Rejected: {619this._handleDidRejectCompletionItem(item);620621// Trigger expected edit capture if enabled622if (this.expectedEditCaptureController.isEnabled && this.expectedEditCaptureController.captureOnReject) {623// Get endpoint info from the log context if available (LLM suggestions only)624const endpointInfo = isLlmCompletionInfo(item.info) ? item.info.suggestion.source.log.endpointInfo : undefined;625const metadata = {626requestUuid: item.info.requestUuid,627providerInfo: item.info.source,628modelName: endpointInfo?.modelName,629endpointUrl: endpointInfo?.url,630suggestionText: item.insertText?.toString(),631suggestionRange: item.range ? [632item.range.start.line,633item.range.start.character,634item.range.end.line,635item.range.end.character636] as [number, number, number, number] : undefined,637documentPath: item.info.documentId.path638};639void this.expectedEditCaptureController.startCapture('rejection', metadata);640}641642break;643}644case InlineCompletionEndOfLifeReasonKind.Ignored: {645const supersededBy = reason.supersededBy ? (reason.supersededBy as NesCompletionItem) : undefined;646logger.trace(`Superseded by: ${supersededBy?.info.requestUuid || 'none'}, was shown: ${item.wasShown}`);647if (supersededBy) {648/* __GDPR__649"supersededInlineEdit" : {650"owner": "ulugbekna",651"comment": "Tracks when an inline edit was superseded by another edit.",652"opportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The opportunity ID of the original inline edit." },653"supersededByOpportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The opportunity ID of the inline edit that superseded the original edit." }654}655*/656this._telemetryService.sendMSFTTelemetryEvent('supersededInlineEdit', { opportunityId: item.info.requestUuid, supersededByOpportunityId: supersededBy.info.requestUuid });657}658this._handleDidIgnoreCompletionItem(item, supersededBy);659break;660}661}662}663664private _handleAcceptance(item: NesCompletionItem) {665this.logContextRecorder?.handleAcceptance(item.info.suggestion);666667item.telemetryBuilder.setAcceptance('accepted');668item.telemetryBuilder.setStatus('accepted');669670const info = item.info;671if (isLlmCompletionInfo(info)) {672this.model.nextEditProvider.handleAcceptance(info.documentId, info.suggestion);673if (!item.isEditInAnotherDocument) {674this._trackSurvivalRate(info);675}676} else {677this.model.diagnosticsBasedProvider?.handleAcceptance(info.documentId, info.suggestion);678}679}680681// TODO: Support tracking Diagnostics NES682private async _trackSurvivalRate(item: LlmCompletionInfo) {683const result = item.suggestion.result;684if (!result || !result.edit) {685return;686}687688const docBeforeEdits = result.documentBeforeEdits.value;689const docAfterEdits = result.edit.toEdit().apply(docBeforeEdits);690691const recorder = this._instantiationService.createInstance(DocumentEditRecorder, item.document);692693// Assumption: The user cannot edit the document while the inline edit is being applied694let userEdits = StringEdit.empty;695// softAssert(docAfterEdits === userEdits.apply(item.document.getText())); // TODO@hediet696697const diffedNextEdit = await stringEditFromDiff(docBeforeEdits, docAfterEdits, this._diffService);698const recordedEdits = recorder.getEdits();699700userEdits = userEdits.compose(recordedEdits);701702this._instantiationService.createInstance(703EditSurvivalReporter,704item.document,705result.documentBeforeEdits.value,706diffedNextEdit,707userEdits,708{ includeArc: true },709res => {710/* __GDPR__711"reportInlineEditSurvivalRate" : {712"owner": "hediet",713"comment": "Reports the survival rate for an inline edit.",714"opportunityId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Unique identifier for an opportunity to show an NES." },715716"survivalRateFourGram": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the AI edit is still present in the document." },717"survivalRateNoRevert": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the ranges the AI touched ended up being reverted." },718"didBranchChange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Indicates if the branch changed in the meantime. If the branch changed (value is 1), this event should probably be ignored." },719"timeDelayMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time delay between the user accepting the edit and measuring the survival rate." },720"arc": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The accepted and restrained character count." }721}722*/723this._telemetryService.sendTelemetryEvent('reportInlineEditSurvivalRate', { microsoft: true, github: { eventNamePrefix: 'copilot-nes/' } },724{725opportunityId: item.requestUuid,726},727{728survivalRateFourGram: res.fourGram,729survivalRateNoRevert: res.noRevert,730didBranchChange: res.didBranchChange ? 1 : 0,731timeDelayMs: res.timeDelayMs,732arc: res.arc!,733}734);735736}737);738}739740private _handleDidRejectCompletionItem(completionItem: NesCompletionItem): void {741this.logContextRecorder?.handleRejection(completionItem.info.suggestion);742743completionItem.telemetryBuilder.setAcceptance('rejected');744completionItem.telemetryBuilder.setStatus('rejected');745746const info = completionItem.info;747if (isLlmCompletionInfo(info)) {748this.model.nextEditProvider.handleRejection(info.documentId, info.suggestion);749} else {750this.model.diagnosticsBasedProvider?.handleRejection(info.documentId, info.suggestion);751}752}753754private _handleDidIgnoreCompletionItem(item: NesCompletionItem, supersededBy?: NesCompletionItem): void {755if (supersededBy) {756item.telemetryBuilder.setSupersededBy(supersededBy.info.requestUuid);757}758759const info = item.info;760const supersededBySuggestion = supersededBy ? supersededBy.info.suggestion : undefined;761if (isLlmCompletionInfo(info)) {762this.model.nextEditProvider.handleIgnored(info.documentId, info.suggestion, supersededBySuggestion);763} else {764this.model.diagnosticsBasedProvider?.handleIgnored(info.documentId, info.suggestion, supersededBySuggestion);765}766}767}768769function hasNotebookCellMarker(document: TextDocument, newText: string) {770return isNotebookCell(document.uri) && newText.includes('%% vscode.cell [id=');771}772773function addNotebookTelemetry(document: TextDocument, position: Position, newText: string, documents: [TextDocument, Range][], telemetryBuilder: NextEditProviderTelemetryBuilder) {774const notebook = isNotebookCell(document.uri) ? findNotebook(document.uri, workspace.notebookDocuments) : undefined;775const cell = notebook ? findCell(document.uri, notebook) : undefined;776if (!cell || !notebook || !documents.length) {777return;778}779const cellMarkerCount = newText.match(/%% vscode.cell \[id=/g)?.length || 0;780const cellMarkerIndex = newText.indexOf('#%% vscode.cell [id=');781const isMultiline = newText.includes('\n');782const targetEol = documents[0][0].eol === EndOfLine.CRLF ? '\r\n' : '\n';783const sourceEol = newText.includes('\r\n') ? '\r\n' : (newText.includes('\n') ? '\n' : targetEol);784const nextEditor = window.visibleTextEditors.find(editor => editor.document === documents[0][0]);785const isNextEditorRangeVisible = nextEditor && nextEditor.visibleRanges.some(range => range.contains(documents[0][1]));786const notebookId = getNotebookId(notebook);787const lineSuffix = `(${position.line}:${position.character})`;788const suggestionLineSuffix = `(->${documents[0][1].start.line}:${documents[0][1].start.character})`;789const getCellPrefix = (c: NotebookCell) => {790if (c === cell) {791return `*`;792}793if (c.document === documents[0][0]) {794return `+`;795}796return '';797};798const lineCounts = notebook.getCells()799.filter(c => c.kind === NotebookCellKind.Code)800.map(c => `${getCellPrefix(c)}${c.document.lineCount}${c === cell ? lineSuffix : ''}${c.document === documents[0][0] ? suggestionLineSuffix : ''}`).join(',');801telemetryBuilder.802setNotebookCellMarkerIndex(cellMarkerIndex)803.setNotebookCellMarkerCount(cellMarkerCount)804.setIsMultilineEdit(isMultiline)805.setIsEolDifferent(targetEol !== sourceEol)806.setIsNextEditorVisible(!!nextEditor)807.setIsNextEditorRangeVisible(!!isNextEditorRangeVisible)808.setNotebookCellLines(lineCounts)809.setNotebookId(notebookId);810}811812813