Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts
4798 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 { booleanComparator, compareBy, compareUndefinedSmallest, numberComparator } from '../../../../../base/common/arrays.js';6import { findLastMax } from '../../../../../base/common/arraysFind.js';7import { RunOnceScheduler } from '../../../../../base/common/async.js';8import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';9import { equalsIfDefined, thisEqualsC } from '../../../../../base/common/equals.js';10import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';11import { cloneAndChange } from '../../../../../base/common/objects.js';12import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, transaction } from '../../../../../base/common/observable.js';13// eslint-disable-next-line local/code-no-deep-import-of-internal14import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js';15import { isDefined, isObject } from '../../../../../base/common/types.js';16import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';17import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';18import { DataChannelForwardingTelemetryService, forwardToChannelIf, isCopilotLikeExtension } from '../../../../../platform/dataChannel/browser/forwardingTelemetryService.js';19import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';20import { ILogService } from '../../../../../platform/log/common/log.js';21import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';22import product from '../../../../../platform/product/common/product.js';23import { StringEdit } from '../../../../common/core/edits/stringEdit.js';24import { Position } from '../../../../common/core/position.js';25import { Range } from '../../../../common/core/range.js';26import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js';27import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';28import { ITextModel } from '../../../../common/model.js';29import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js';30import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';31import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';32import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js';33import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry } from '../telemetry.js';34import { wait } from '../utils.js';35import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js';36import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js';37import { RenameSymbolProcessor } from './renameSymbolProcessor.js';3839export class InlineCompletionsSource extends Disposable {40private static _requestId = 0;4142private readonly _updateOperation = this._register(new MutableDisposable<UpdateOperation>());4344private readonly _loggingEnabled;45private readonly _sendRequestData;4647private readonly _structuredFetchLogger;4849private readonly _state = observableReducerSettable(this, {50initial: () => ({51inlineCompletions: InlineCompletionsState.createEmpty(),52suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),53}),54disposeFinal: (values) => {55values.inlineCompletions.dispose();56values.suggestWidgetInlineCompletions.dispose();57},58changeTracker: recordChangesLazy(() => ({ versionId: this._versionId })),59update: (reader, previousValue, changes) => {60const edit = StringEdit.compose(changes.changes.map(c => c.change ? offsetEditFromContentChanges(c.change.changes) : StringEdit.empty).filter(isDefined));6162if (edit.isEmpty()) {63return previousValue;64}65try {66return {67inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel),68suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel),69};70} finally {71previousValue.inlineCompletions.dispose();72previousValue.suggestWidgetInlineCompletions.dispose();73}74}75});7677public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions);78public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions);7980private readonly _renameProcessor: RenameSymbolProcessor;8182private _completionsEnabled: Record<string, boolean> | undefined = undefined;8384constructor(85private readonly _textModel: ITextModel,86private readonly _versionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>,87private readonly _debounceValue: IFeatureDebounceInformation,88private readonly _cursorPosition: IObservable<Position>,89@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,90@ILogService private readonly _logService: ILogService,91@IConfigurationService private readonly _configurationService: IConfigurationService,92@IInstantiationService private readonly _instantiationService: IInstantiationService,93@IContextKeyService private readonly _contextKeyService: IContextKeyService,94) {95super();96this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store);97this._sendRequestData = observableConfigValue('editor.inlineSuggest.emptyResponseInformation', true, this._configurationService).recomputeInitiallyAndOnChange(this._store);98this._structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast<99{ kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry100| { kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry101>(),102'editor.inlineSuggest.logFetch.commandId'103));104105this._renameProcessor = this._store.add(this._instantiationService.createInstance(RenameSymbolProcessor));106107this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store);108109const enablementSetting = product.defaultChatAgent?.completionsEnablementSetting ?? undefined;110if (enablementSetting) {111this._updateCompletionsEnablement(enablementSetting);112this._register(this._configurationService.onDidChangeConfiguration(e => {113if (e.affectsConfiguration(enablementSetting)) {114this._updateCompletionsEnablement(enablementSetting);115}116}));117}118119this._state.recomputeInitiallyAndOnChange(this._store);120}121122private _updateCompletionsEnablement(enalementSetting: string) {123const result = this._configurationService.getValue<Record<string, boolean>>(enalementSetting);124if (!isObject(result)) {125this._completionsEnabled = undefined;126} else {127this._completionsEnabled = result;128}129}130131public readonly clearOperationOnTextModelChange = derived(this, reader => {132this._versionId.read(reader);133this._updateOperation.clear();134return undefined; // always constant135});136137private _log(entry:138{ sourceId: string; kind: 'start'; requestId: number; context: unknown; provider: string | undefined } & IRecordableEditorLogEntry139| { sourceId: string; kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number; didAllProvidersReturn: boolean } & IRecordableLogEntry140) {141if (this._loggingEnabled.get()) {142this._logService.info(formatRecordableLogEntry(entry));143}144this._structuredFetchLogger.log(entry);145}146147private readonly _loadingCount = observableValue(this, 0);148public readonly loading = this._loadingCount.map(this, v => v > 0);149150public fetch(151providers: InlineCompletionsProvider[],152providersLabel: string | undefined,153context: InlineCompletionContextWithoutUuid,154activeInlineCompletion: InlineSuggestionIdentity | undefined,155withDebounce: boolean,156userJumpedToActiveCompletion: IObservable<boolean>,157requestInfo: InlineSuggestRequestInfo158): Promise<boolean> {159const position = this._cursorPosition.get();160const request = new UpdateRequest(position, context, this._textModel.getVersionId(), new Set(providers));161162const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions.get() : this.inlineCompletions.get();163164if (this._updateOperation.value?.request.satisfies(request)) {165return this._updateOperation.value.promise;166} else if (target?.request?.satisfies(request)) {167return Promise.resolve(true);168}169170const updateOngoing = !!this._updateOperation.value;171this._updateOperation.clear();172173const source = new CancellationTokenSource();174175const promise = (async () => {176const store = new DisposableStore();177178this._loadingCount.set(this._loadingCount.get() + 1, undefined);179let didDecrease = false;180const decreaseLoadingCount = () => {181if (!didDecrease) {182didDecrease = true;183this._loadingCount.set(this._loadingCount.get() - 1, undefined);184}185};186const loadingReset = store.add(new RunOnceScheduler(() => decreaseLoadingCount(), 10 * 1000));187loadingReset.schedule();188189const inlineSuggestionsProviders = providers.filter(p => p.providerId);190const requestResponseInfo = new RequestResponseData(context, requestInfo, inlineSuggestionsProviders);191192193try {194const recommendedDebounceValue = this._debounceValue.get(this._textModel);195const debounceValue = findLastMax(196providers.map(p => p.debounceDelayMs),197compareUndefinedSmallest(numberComparator)198) ?? recommendedDebounceValue;199200// Debounce in any case if update is ongoing201const shouldDebounce = updateOngoing || (withDebounce && context.triggerKind === InlineCompletionTriggerKind.Automatic);202if (shouldDebounce) {203// This debounces the operation204await wait(debounceValue, source.token);205}206207if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) {208requestResponseInfo.setNoSuggestionReasonIfNotSet('canceled:beforeFetch');209return false;210}211212const requestId = InlineCompletionsSource._requestId++;213if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) {214this._log({215sourceId: 'InlineCompletions.fetch',216kind: 'start',217requestId,218modelUri: this._textModel.uri,219modelVersion: this._textModel.getVersionId(),220context: { triggerKind: context.triggerKind, suggestInfo: context.selectedSuggestionInfo ? true : undefined },221time: Date.now(),222provider: providersLabel,223});224}225226const startTime = new Date();227const providerResult = provideInlineCompletions(providers, this._cursorPosition.get(), this._textModel, context, requestInfo, this._languageConfigurationService);228229runWhenCancelled(source.token, () => providerResult.cancelAndDispose({ kind: 'tokenCancellation' }));230231let shouldStopEarly = false;232let producedSuggestion = false;233234const providerSuggestions: InlineSuggestionItem[] = [];235for await (const list of providerResult.lists) {236if (!list) {237continue;238}239list.addRef();240store.add(toDisposable(() => list.removeRef(list.inlineSuggestionsData.length === 0 ? { kind: 'empty' } : { kind: 'notTaken' })));241242for (const item of list.inlineSuggestionsData) {243producedSuggestion = true;244if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) {245item.setNotShownReason('notInlineEditRequested');246continue;247}248if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) {249item.setNotShownReason('notInlineCompletionRequested');250continue;251}252253item.addPerformanceMarker('providerReturned');254const i = InlineSuggestionItem.create(item, this._textModel);255item.addPerformanceMarker('itemCreated');256providerSuggestions.push(i);257// Stop after first visible inline completion258if (!i.isInlineEdit && !i.showInlineEditMenu && context.triggerKind === InlineCompletionTriggerKind.Automatic) {259if (i.isVisible(this._textModel, this._cursorPosition.get())) {260shouldStopEarly = true;261}262}263}264265if (shouldStopEarly) {266break;267}268}269270providerSuggestions.forEach(s => s.addPerformanceMarker('providersResolved'));271272const suggestions: InlineSuggestionItem[] = await Promise.all(providerSuggestions.map(async s => {273return this._renameProcessor.proposeRenameRefactoring(this._textModel, s, context);274}));275276suggestions.forEach(s => s.addPerformanceMarker('renameProcessed'));277278providerResult.cancelAndDispose({ kind: 'lostRace' });279280if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) {281const didAllProvidersReturn = providerResult.didAllProvidersReturn;282let error: string | undefined = undefined;283if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) {284error = 'canceled';285}286const result = suggestions.map(c => {287const comp = c.getSourceCompletion();288if (comp.doNotLog) {289return undefined;290}291const obj = {292insertText: comp.insertText,293range: comp.range,294additionalTextEdits: comp.additionalTextEdits,295uri: comp.uri,296command: comp.command,297gutterMenuLinkAction: comp.gutterMenuLinkAction,298shownCommand: comp.shownCommand,299completeBracketPairs: comp.completeBracketPairs,300isInlineEdit: comp.isInlineEdit,301showInlineEditMenu: comp.showInlineEditMenu,302showRange: comp.showRange,303warning: comp.warning,304hint: comp.hint,305supportsRename: comp.supportsRename,306correlationId: comp.correlationId,307jumpToPosition: comp.jumpToPosition,308};309return {310...(cloneAndChange(obj, v => {311if (Range.isIRange(v)) {312return Range.lift(v).toString();313}314if (Position.isIPosition(v)) {315return Position.lift(v).toString();316}317if (Command.is(v)) {318return { $commandId: v.id };319}320return v;321}) as object),322$providerId: c.source.provider.providerId?.toString(),323};324}).filter(result => result !== undefined);325326this._log({ sourceId: 'InlineCompletions.fetch', kind: 'end', requestId, durationMs: (Date.now() - startTime.getTime()), error, result, time: Date.now(), didAllProvidersReturn });327}328329requestResponseInfo.setRequestUuid(providerResult.contextWithUuid.requestUuid);330if (producedSuggestion) {331requestResponseInfo.setHasProducedSuggestion();332if (suggestions.length > 0 && source.token.isCancellationRequested) {333suggestions.forEach(s => s.setNotShownReasonIfNotSet('canceled:whileAwaitingOtherProviders'));334}335} else {336if (source.token.isCancellationRequested) {337requestResponseInfo.setNoSuggestionReasonIfNotSet('canceled:whileFetching');338} else {339const completionsQuotaExceeded = this._contextKeyService.getContextKeyValue<boolean>('completionsQuotaExceeded');340requestResponseInfo.setNoSuggestionReasonIfNotSet(completionsQuotaExceeded ? 'completionsQuotaExceeded' : 'noSuggestion');341}342}343344const remainingTimeToWait = context.earliestShownDateTime - Date.now();345if (remainingTimeToWait > 0) {346await wait(remainingTimeToWait, source.token);347}348349suggestions.forEach(s => s.addPerformanceMarker('minShowDelayPassed'));350351if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId352|| userJumpedToActiveCompletion.get() /* In the meantime the user showed interest for the active completion so dont hide it */) {353const notShownReason =354source.token.isCancellationRequested ? 'canceled:afterMinShowDelay' :355this._store.isDisposed ? 'canceled:disposed' :356this._textModel.getVersionId() !== request.versionId ? 'canceled:documentChanged' :357userJumpedToActiveCompletion.get() ? 'canceled:userJumped' :358'unknown';359suggestions.forEach(s => s.setNotShownReasonIfNotSet(notShownReason));360return false;361}362363const endTime = new Date();364this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime());365366const cursorPosition = this._cursorPosition.get();367this._updateOperation.clear();368transaction(tx => {369/** @description Update completions with provider result */370const v = this._state.get();371372if (context.selectedSuggestionInfo) {373this._state.set({374inlineCompletions: InlineCompletionsState.createEmpty(),375suggestWidgetInlineCompletions: v.suggestWidgetInlineCompletions.createStateWithAppliedResults(suggestions, request, this._textModel, cursorPosition, activeInlineCompletion),376}, tx);377} else {378this._state.set({379inlineCompletions: v.inlineCompletions.createStateWithAppliedResults(suggestions, request, this._textModel, cursorPosition, activeInlineCompletion),380suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),381}, tx);382}383384v.inlineCompletions.dispose();385v.suggestWidgetInlineCompletions.dispose();386});387} finally {388store.dispose();389decreaseLoadingCount();390this.sendInlineCompletionsRequestTelemetry(requestResponseInfo);391}392393return true;394})();395396const updateOperation = new UpdateOperation(request, source, promise);397this._updateOperation.value = updateOperation;398399return promise;400}401402public clear(tx: ITransaction): void {403this._updateOperation.clear();404const v = this._state.get();405this._state.set({406inlineCompletions: InlineCompletionsState.createEmpty(),407suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty()408}, tx);409v.inlineCompletions.dispose();410v.suggestWidgetInlineCompletions.dispose();411}412413public seedInlineCompletionsWithSuggestWidget(): void {414const inlineCompletions = this.inlineCompletions.get();415const suggestWidgetInlineCompletions = this.suggestWidgetInlineCompletions.get();416if (!suggestWidgetInlineCompletions) {417return;418}419transaction(tx => {420/** @description Seed inline completions with (newer) suggest widget inline completions */421if (!inlineCompletions || (suggestWidgetInlineCompletions.request?.versionId ?? -1) > (inlineCompletions.request?.versionId ?? -1)) {422inlineCompletions?.dispose();423const s = this._state.get();424this._state.set({425inlineCompletions: suggestWidgetInlineCompletions.clone(),426suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),427}, tx);428s.inlineCompletions.dispose();429s.suggestWidgetInlineCompletions.dispose();430}431this.clearSuggestWidgetInlineCompletions(tx);432});433}434435private sendInlineCompletionsRequestTelemetry(436requestResponseInfo: RequestResponseData437): void {438if (!this._sendRequestData.get() && !this._contextKeyService.getContextKeyValue<boolean>('isRunningUnificationExperiment')) {439return;440}441442if (requestResponseInfo.requestUuid === undefined || requestResponseInfo.hasProducedSuggestion) {443return;444}445446447if (!isCompletionsEnabled(this._completionsEnabled, this._textModel.getLanguageId())) {448return;449}450451if (!requestResponseInfo.providers.some(p => isCopilotLikeExtension(p.providerId?.extensionId))) {452return;453}454455const emptyEndOfLifeEvent: InlineCompletionEndOfLifeEvent = {456opportunityId: requestResponseInfo.requestUuid,457noSuggestionReason: requestResponseInfo.noSuggestionReason ?? 'unknown',458extensionId: 'vscode-core',459extensionVersion: '0.0.0',460groupId: 'empty',461shown: false,462skuPlan: requestResponseInfo.requestInfo.sku?.plan,463skuType: requestResponseInfo.requestInfo.sku?.type,464editorType: requestResponseInfo.requestInfo.editorType,465requestReason: requestResponseInfo.requestInfo.reason,466typingInterval: requestResponseInfo.requestInfo.typingInterval,467typingIntervalCharacterCount: requestResponseInfo.requestInfo.typingIntervalCharacterCount,468languageId: requestResponseInfo.requestInfo.languageId,469selectedSuggestionInfo: !!requestResponseInfo.context.selectedSuggestionInfo,470availableProviders: requestResponseInfo.providers.map(p => p.providerId?.toString()).filter(isDefined).join(','),471...forwardToChannelIf(requestResponseInfo.providers.some(p => isCopilotLikeExtension(p.providerId?.extensionId))),472timeUntilProviderRequest: undefined,473timeUntilProviderResponse: undefined,474viewKind: undefined,475preceeded: undefined,476superseded: undefined,477reason: undefined,478acceptedAlternativeAction: undefined,479correlationId: undefined,480shownDuration: undefined,481shownDurationUncollapsed: undefined,482timeUntilShown: undefined,483partiallyAccepted: undefined,484partiallyAcceptedCountSinceOriginal: undefined,485partiallyAcceptedRatioSinceOriginal: undefined,486partiallyAcceptedCharactersSinceOriginal: undefined,487cursorColumnDistance: undefined,488cursorLineDistance: undefined,489lineCountOriginal: undefined,490lineCountModified: undefined,491characterCountOriginal: undefined,492characterCountModified: undefined,493disjointReplacements: undefined,494sameShapeReplacements: undefined,495longDistanceHintVisible: undefined,496longDistanceHintDistance: undefined,497notShownReason: undefined,498renameCreated: false,499renameDuration: undefined,500renameTimedOut: false,501renameDroppedOtherEdits: undefined,502renameDroppedRenameEdits: undefined,503performanceMarkers: undefined,504editKind: undefined,505};506507const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService);508sendInlineCompletionsEndOfLifeTelemetry(dataChannel, emptyEndOfLifeEvent);509}510511public clearSuggestWidgetInlineCompletions(tx: ITransaction): void {512if (this._updateOperation.value?.request.context.selectedSuggestionInfo) {513this._updateOperation.clear();514}515}516517public cancelUpdate(): void {518this._updateOperation.clear();519}520}521522class UpdateRequest {523constructor(524public readonly position: Position,525public readonly context: InlineCompletionContextWithoutUuid,526public readonly versionId: number,527public readonly providers: Set<InlineCompletionsProvider>,528) {529}530531public satisfies(other: UpdateRequest): boolean {532return this.position.equals(other.position)533&& equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, thisEqualsC())534&& (other.context.triggerKind === InlineCompletionTriggerKind.Automatic535|| this.context.triggerKind === InlineCompletionTriggerKind.Explicit)536&& this.versionId === other.versionId537&& isSubset(other.providers, this.providers);538}539540public get isExplicitRequest() {541return this.context.triggerKind === InlineCompletionTriggerKind.Explicit;542}543}544545class RequestResponseData {546public requestUuid: string | undefined;547public noSuggestionReason: string | undefined;548public hasProducedSuggestion = false;549550constructor(551public readonly context: InlineCompletionContextWithoutUuid,552public readonly requestInfo: InlineSuggestRequestInfo,553public readonly providers: InlineCompletionsProvider[],554) { }555556setRequestUuid(uuid: string) {557this.requestUuid = uuid;558}559560setNoSuggestionReasonIfNotSet(type: string) {561this.noSuggestionReason ??= type;562}563564setHasProducedSuggestion() {565this.hasProducedSuggestion = true;566}567}568569function isSubset<T>(set1: Set<T>, set2: Set<T>): boolean {570return [...set1].every(item => set2.has(item));571}572573function isCompletionsEnabled(completionsEnablementObject: Record<string, boolean> | undefined, modeId: string = '*'): boolean {574if (completionsEnablementObject === undefined) {575return false; // default to disabled if setting is not available576}577578if (typeof completionsEnablementObject[modeId] !== 'undefined') {579return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined580}581582return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise583}584585class UpdateOperation implements IDisposable {586constructor(587public readonly request: UpdateRequest,588public readonly cancellationTokenSource: CancellationTokenSource,589public readonly promise: Promise<boolean>,590) {591}592593dispose() {594this.cancellationTokenSource.cancel();595}596}597598class InlineCompletionsState extends Disposable {599public static createEmpty(): InlineCompletionsState {600return new InlineCompletionsState([], undefined);601}602603constructor(604public readonly inlineCompletions: readonly InlineSuggestionItem[],605public readonly request: UpdateRequest | undefined,606) {607for (const inlineCompletion of inlineCompletions) {608inlineCompletion.addRef();609}610611super();612613this._register({614dispose: () => {615for (const inlineCompletion of this.inlineCompletions) {616inlineCompletion.removeRef();617}618}619});620}621622private _findById(id: InlineSuggestionIdentity): InlineSuggestionItem | undefined {623return this.inlineCompletions.find(i => i.identity === id);624}625626private _findByHash(hash: string): InlineSuggestionItem | undefined {627return this.inlineCompletions.find(i => i.hash === hash);628}629630/**631* Applies the edit on the state.632*/633public createStateWithAppliedEdit(edit: StringEdit, textModel: ITextModel): InlineCompletionsState {634const newInlineCompletions = this.inlineCompletions.map(i => i.withEdit(edit, textModel)).filter(isDefined);635return new InlineCompletionsState(newInlineCompletions, this.request);636}637638public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserveAtTop: InlineSuggestionIdentity | undefined): InlineCompletionsState {639let itemToPreserve: InlineSuggestionItem | undefined = undefined;640if (itemIdToPreserveAtTop) {641const itemToPreserveCandidate = this._findById(itemIdToPreserveAtTop);642if (itemToPreserveCandidate && itemToPreserveCandidate.canBeReused(textModel, request.position)) {643itemToPreserve = itemToPreserveCandidate;644645const updatedItemToPreserve = updatedSuggestions.find(i => i.hash === itemToPreserveCandidate.hash);646if (updatedItemToPreserve) {647updatedSuggestions = moveToFront(updatedItemToPreserve, updatedSuggestions);648} else {649updatedSuggestions = [itemToPreserveCandidate, ...updatedSuggestions];650}651}652}653654const preferInlineCompletions = itemToPreserve655// itemToPreserve has precedence656? !itemToPreserve.isInlineEdit657// Otherwise: prefer inline completion if there is a visible one658: updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition));659660let updatedItems: InlineSuggestionItem[] = [];661for (const i of updatedSuggestions) {662const oldItem = this._findByHash(i.hash);663let item;664if (oldItem && oldItem !== i) {665item = i.withIdentity(oldItem.identity);666i.setIsPreceeded(oldItem);667oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() });668} else {669item = i;670}671if (preferInlineCompletions !== item.isInlineEdit) {672updatedItems.push(item);673}674}675676updatedItems.sort(compareBy(i => i.showInlineEditMenu, booleanComparator));677updatedItems = distinctByKey(updatedItems, i => i.semanticId);678679return new InlineCompletionsState(updatedItems, request);680}681682public clone(): InlineCompletionsState {683return new InlineCompletionsState(this.inlineCompletions, this.request);684}685}686687/** Keeps the first item in case of duplicates. */688function distinctByKey<T>(items: T[], key: (item: T) => unknown): T[] {689const seen = new Set();690return items.filter(item => {691const k = key(item);692if (seen.has(k)) {693return false;694}695seen.add(k);696return true;697});698}699700function moveToFront<T>(item: T, items: T[]): T[] {701const index = items.indexOf(item);702if (index > -1) {703return [item, ...items.slice(0, index), ...items.slice(index + 1)];704}705return items;706}707708709