Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts
5281 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, runOnChange, 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 { isCompletionsEnabledFromObject } from '../../../../common/services/completionsEnablement.js';31import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';32import { ITextModelService } from '../../../../common/services/resolverService.js';33import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';34import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js';35import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry } from '../telemetry.js';36import { wait } from '../utils.js';37import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js';38import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js';39import { RenameSymbolProcessor } from './renameSymbolProcessor.js';40import { TextModelValueReference } from './textModelValueReference.js';4142export class InlineCompletionsSource extends Disposable {43private static _requestId = 0;4445private readonly _updateOperation = this._register(new MutableDisposable<UpdateOperation>());4647private readonly _loggingEnabled;48private readonly _sendRequestData;4950private readonly _structuredFetchLogger;5152private readonly _state = observableReducerSettable(this, {53initial: () => ({54inlineCompletions: InlineCompletionsState.createEmpty(),55suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),56}),57disposeFinal: (values) => {58values.inlineCompletions.dispose();59values.suggestWidgetInlineCompletions.dispose();60},61changeTracker: recordChangesLazy(() => ({ versionId: this._versionId })),62update: (reader, previousValue, changes) => {63const edit = StringEdit.compose(changes.changes.map(c => c.change ? offsetEditFromContentChanges(c.change.changes) : StringEdit.empty).filter(isDefined));6465if (edit.isEmpty()) {66return previousValue;67}68try {69return {70inlineCompletions: previousValue.inlineCompletions.createStateWithAppliedEdit(edit, this._textModel),71suggestWidgetInlineCompletions: previousValue.suggestWidgetInlineCompletions.createStateWithAppliedEdit(edit, this._textModel),72};73} finally {74previousValue.inlineCompletions.dispose();75previousValue.suggestWidgetInlineCompletions.dispose();76}77}78});7980public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions);81public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions);8283private readonly _renameProcessor: RenameSymbolProcessor;8485private _completionsEnabled: Record<string, boolean> | undefined = undefined;8687constructor(88private readonly _textModel: ITextModel,89private readonly _versionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>,90private readonly _debounceValue: IFeatureDebounceInformation,91private readonly _cursorPosition: IObservable<Position>,92@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,93@ILogService private readonly _logService: ILogService,94@IConfigurationService private readonly _configurationService: IConfigurationService,95@IInstantiationService private readonly _instantiationService: IInstantiationService,96@IContextKeyService private readonly _contextKeyService: IContextKeyService,97@ITextModelService private readonly _textModelService: ITextModelService,98) {99super();100this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store);101this._sendRequestData = observableConfigValue('editor.inlineSuggest.emptyResponseInformation', true, this._configurationService).recomputeInitiallyAndOnChange(this._store);102this._structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast<103{ kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry104| { kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry105>(),106'editor.inlineSuggest.logFetch.commandId'107));108109this._renameProcessor = this._store.add(this._instantiationService.createInstance(RenameSymbolProcessor));110111this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store);112113const enablementSetting = product.defaultChatAgent?.completionsEnablementSetting ?? undefined;114if (enablementSetting) {115this._updateCompletionsEnablement(enablementSetting);116this._register(this._configurationService.onDidChangeConfiguration(e => {117if (e.affectsConfiguration(enablementSetting)) {118this._updateCompletionsEnablement(enablementSetting);119}120}));121}122123this._state.recomputeInitiallyAndOnChange(this._store);124}125126private _updateCompletionsEnablement(enalementSetting: string) {127const result = this._configurationService.getValue<Record<string, boolean>>(enalementSetting);128if (!isObject(result)) {129this._completionsEnabled = undefined;130} else {131this._completionsEnabled = result;132}133}134135public readonly clearOperationOnTextModelChange = derived(this, reader => {136this._versionId.read(reader);137this._updateOperation.clear();138return undefined; // always constant139});140141private _log(entry:142{ sourceId: string; kind: 'start'; requestId: number; context: unknown; provider: string | undefined } & IRecordableEditorLogEntry143| { sourceId: string; kind: 'end'; error: unknown; durationMs: number; result: unknown; requestId: number; didAllProvidersReturn: boolean } & IRecordableLogEntry144) {145if (this._loggingEnabled.get()) {146this._logService.info(formatRecordableLogEntry(entry));147}148this._structuredFetchLogger.log(entry);149}150151private readonly _loadingCount = observableValue(this, 0);152public readonly loading = this._loadingCount.map(this, v => v > 0);153154public fetch(155providers: InlineCompletionsProvider[],156providersLabel: string | undefined,157context: InlineCompletionContextWithoutUuid,158activeInlineCompletion: InlineSuggestionIdentity | undefined,159withDebounce: boolean,160userJumpedToActiveCompletion: IObservable<boolean>,161requestInfo: InlineSuggestRequestInfo162): Promise<boolean> {163const position = this._cursorPosition.get();164const request = new UpdateRequest(position, context, this._textModel.getVersionId(), new Set(providers));165166const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions.get() : this.inlineCompletions.get();167168if (this._updateOperation.value?.request.satisfies(request)) {169return this._updateOperation.value.promise;170} else if (target?.request?.satisfies(request)) {171return Promise.resolve(true);172}173174const updateOngoing = !!this._updateOperation.value;175this._updateOperation.clear();176177const source = new CancellationTokenSource();178179const promise = (async () => {180const store = new DisposableStore();181182this._loadingCount.set(this._loadingCount.get() + 1, undefined);183let didDecrease = false;184const decreaseLoadingCount = () => {185if (!didDecrease) {186didDecrease = true;187this._loadingCount.set(this._loadingCount.get() - 1, undefined);188}189};190const loadingReset = store.add(new RunOnceScheduler(() => decreaseLoadingCount(), 10 * 1000));191loadingReset.schedule();192193const inlineSuggestionsProviders = providers.filter(p => p.providerId);194const requestResponseInfo = new RequestResponseData(context, requestInfo, inlineSuggestionsProviders);195196197try {198const recommendedDebounceValue = this._debounceValue.get(this._textModel);199const debounceValue = findLastMax(200providers.map(p => p.debounceDelayMs),201compareUndefinedSmallest(numberComparator)202) ?? recommendedDebounceValue;203204// Debounce in any case if update is ongoing205const shouldDebounce = updateOngoing || (withDebounce && context.triggerKind === InlineCompletionTriggerKind.Automatic);206if (shouldDebounce) {207// This debounces the operation208await wait(debounceValue, source.token);209}210211if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) {212requestResponseInfo.setNoSuggestionReasonIfNotSet('canceled:beforeFetch');213return false;214}215216const requestId = InlineCompletionsSource._requestId++;217if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) {218this._log({219sourceId: 'InlineCompletions.fetch',220kind: 'start',221requestId,222modelUri: this._textModel.uri,223modelVersion: this._textModel.getVersionId(),224context: { triggerKind: context.triggerKind, suggestInfo: context.selectedSuggestionInfo ? true : undefined },225time: Date.now(),226provider: providersLabel,227});228}229230const startTime = new Date();231const providerResult = provideInlineCompletions(providers, this._cursorPosition.get(), this._textModel, context, requestInfo, this._languageConfigurationService);232233runWhenCancelled(source.token, () => providerResult.cancelAndDispose({ kind: 'tokenCancellation' }));234235let shouldStopEarly = false;236let producedSuggestion = false;237238const providerSuggestions: InlineSuggestionItem[] = [];239for await (const list of providerResult.lists) {240if (!list) {241continue;242}243list.addRef();244store.add(toDisposable(() => list.removeRef(list.inlineSuggestionsData.length === 0 ? { kind: 'empty' } : { kind: 'notTaken' })));245246for (const item of list.inlineSuggestionsData) {247producedSuggestion = true;248if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) {249item.setNotShownReason('notInlineEditRequested');250continue;251}252if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) {253item.setNotShownReason('notInlineCompletionRequested');254continue;255}256257item.addPerformanceMarker('providerReturned');258259const targetUri = item.action?.uri;260let targetModel: ITextModel;261let disposable: IDisposable | undefined;262263if (targetUri && targetUri.toString() !== this._textModel.uri.toString()) {264const modelRef = await this._textModelService.createModelReference(targetUri);265targetModel = modelRef.object.textEditorModel;266disposable = modelRef;267} else {268targetModel = this._textModel;269disposable = undefined;270}271272const ref = TextModelValueReference.snapshot(targetModel);273274const i = InlineSuggestionItem.create(item, ref);275if (disposable) {276const s = runOnChange(i.identity.onDispose, () => {277disposable?.dispose();278s.dispose();279});280}281282item.addPerformanceMarker('itemCreated');283providerSuggestions.push(i);284// Stop after first visible inline completion285if (!i.isInlineEdit && !i.showInlineEditMenu && context.triggerKind === InlineCompletionTriggerKind.Automatic) {286if (i.isVisible(this._textModel, this._cursorPosition.get())) {287shouldStopEarly = true;288}289}290}291292if (shouldStopEarly) {293break;294}295}296297providerSuggestions.forEach(s => s.addPerformanceMarker('providersResolved'));298299const suggestions: InlineSuggestionItem[] = await Promise.all(providerSuggestions.map(async s => {300return this._renameProcessor.proposeRenameRefactoring(this._textModel, s, context);301}));302303suggestions.forEach(s => s.addPerformanceMarker('renameProcessed'));304305providerResult.cancelAndDispose({ kind: 'lostRace' });306307if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) {308const didAllProvidersReturn = providerResult.didAllProvidersReturn;309let error: string | undefined = undefined;310if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) {311error = 'canceled';312}313const result = suggestions.map(c => {314const comp = c.getSourceCompletion();315if (comp.doNotLog) {316return undefined;317}318const obj = {319insertText: comp.insertText,320range: comp.range,321additionalTextEdits: comp.additionalTextEdits,322uri: comp.uri,323command: comp.command,324gutterMenuLinkAction: comp.gutterMenuLinkAction,325shownCommand: comp.shownCommand,326completeBracketPairs: comp.completeBracketPairs,327isInlineEdit: comp.isInlineEdit,328showInlineEditMenu: comp.showInlineEditMenu,329showRange: comp.showRange,330warning: comp.warning,331hint: comp.hint,332supportsRename: comp.supportsRename,333correlationId: comp.correlationId,334jumpToPosition: comp.jumpToPosition,335};336return {337...(cloneAndChange(obj, v => {338if (Range.isIRange(v)) {339return Range.lift(v).toString();340}341if (Position.isIPosition(v)) {342return Position.lift(v).toString();343}344if (Command.is(v)) {345return { $commandId: v.id };346}347return v;348}) as object),349$providerId: c.source.provider.providerId?.toString(),350};351}).filter(result => result !== undefined);352353this._log({ sourceId: 'InlineCompletions.fetch', kind: 'end', requestId, durationMs: (Date.now() - startTime.getTime()), error, result, time: Date.now(), didAllProvidersReturn });354}355356requestResponseInfo.setRequestUuid(providerResult.contextWithUuid.requestUuid);357if (producedSuggestion) {358requestResponseInfo.setHasProducedSuggestion();359if (suggestions.length > 0 && source.token.isCancellationRequested) {360suggestions.forEach(s => s.setNotShownReasonIfNotSet('canceled:whileAwaitingOtherProviders'));361}362} else {363if (source.token.isCancellationRequested) {364requestResponseInfo.setNoSuggestionReasonIfNotSet('canceled:whileFetching');365} else {366const completionsQuotaExceeded = this._contextKeyService.getContextKeyValue<boolean>('completionsQuotaExceeded');367requestResponseInfo.setNoSuggestionReasonIfNotSet(completionsQuotaExceeded ? 'completionsQuotaExceeded' : 'noSuggestion');368}369}370371const remainingTimeToWait = context.earliestShownDateTime - Date.now();372if (remainingTimeToWait > 0) {373await wait(remainingTimeToWait, source.token);374}375376suggestions.forEach(s => s.addPerformanceMarker('minShowDelayPassed'));377378if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId379|| userJumpedToActiveCompletion.get() /* In the meantime the user showed interest for the active completion so dont hide it */) {380const notShownReason =381source.token.isCancellationRequested ? 'canceled:afterMinShowDelay' :382this._store.isDisposed ? 'canceled:disposed' :383this._textModel.getVersionId() !== request.versionId ? 'canceled:documentChanged' :384userJumpedToActiveCompletion.get() ? 'canceled:userJumped' :385'unknown';386suggestions.forEach(s => s.setNotShownReasonIfNotSet(notShownReason));387return false;388}389390const endTime = new Date();391this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime());392393const cursorPosition = this._cursorPosition.get();394this._updateOperation.clear();395transaction(tx => {396/** @description Update completions with provider result */397const v = this._state.get();398399if (context.selectedSuggestionInfo) {400this._state.set({401inlineCompletions: InlineCompletionsState.createEmpty(),402suggestWidgetInlineCompletions: v.suggestWidgetInlineCompletions.createStateWithAppliedResults(suggestions, request, this._textModel, cursorPosition, activeInlineCompletion),403}, tx);404} else {405this._state.set({406inlineCompletions: v.inlineCompletions.createStateWithAppliedResults(suggestions, request, this._textModel, cursorPosition, activeInlineCompletion),407suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),408}, tx);409}410411v.inlineCompletions.dispose();412v.suggestWidgetInlineCompletions.dispose();413});414} finally {415store.dispose();416decreaseLoadingCount();417this._sendInlineCompletionsRequestTelemetry(requestResponseInfo);418}419420return true;421})();422423const updateOperation = new UpdateOperation(request, source, promise);424this._updateOperation.value = updateOperation;425426return promise;427}428429public clear(tx: ITransaction): void {430if (this._store.isDisposed) {431return;432}433this._updateOperation.clear();434const v = this._state.get();435this._state.set({436inlineCompletions: InlineCompletionsState.createEmpty(),437suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty()438}, tx);439v.inlineCompletions.dispose();440v.suggestWidgetInlineCompletions.dispose();441}442443public seedInlineCompletionsWithSuggestWidget(): void {444const inlineCompletions = this.inlineCompletions.get();445const suggestWidgetInlineCompletions = this.suggestWidgetInlineCompletions.get();446if (!suggestWidgetInlineCompletions) {447return;448}449transaction(tx => {450/** @description Seed inline completions with (newer) suggest widget inline completions */451if (!inlineCompletions || (suggestWidgetInlineCompletions.request?.versionId ?? -1) > (inlineCompletions.request?.versionId ?? -1)) {452inlineCompletions?.dispose();453const s = this._state.get();454this._state.set({455inlineCompletions: suggestWidgetInlineCompletions.clone(),456suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),457}, tx);458s.inlineCompletions.dispose();459s.suggestWidgetInlineCompletions.dispose();460}461this.clearSuggestWidgetInlineCompletions(tx);462});463}464465/**466* Seeds the inline completions with an external inline completion item.467* Used when transplanting a completion from one model to another (cross-file edits).468*/469public seedWithCompletion(item: InlineSuggestionItem, tx: ITransaction): void {470const s = this._state.get();471this._state.set({472inlineCompletions: new InlineCompletionsState([item], undefined),473suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),474}, tx);475s.inlineCompletions.dispose();476s.suggestWidgetInlineCompletions.dispose();477}478479private _sendInlineCompletionsRequestTelemetry(480requestResponseInfo: RequestResponseData481): void {482if (!this._sendRequestData.get() && !this._contextKeyService.getContextKeyValue<boolean>('isRunningUnificationExperiment')) {483return;484}485486if (requestResponseInfo.requestUuid === undefined || requestResponseInfo.hasProducedSuggestion) {487return;488}489490491if (!isCompletionsEnabledFromObject(this._completionsEnabled, this._textModel.getLanguageId())) {492return;493}494495if (!requestResponseInfo.providers.some(p => isCopilotLikeExtension(p.providerId?.extensionId))) {496return;497}498499const emptyEndOfLifeEvent: InlineCompletionEndOfLifeEvent = {500opportunityId: requestResponseInfo.requestUuid,501noSuggestionReason: requestResponseInfo.noSuggestionReason ?? 'unknown',502extensionId: 'vscode-core',503extensionVersion: '0.0.0',504groupId: 'empty',505shown: false,506skuPlan: requestResponseInfo.requestInfo.sku?.plan,507skuType: requestResponseInfo.requestInfo.sku?.type,508editorType: requestResponseInfo.requestInfo.editorType,509requestReason: requestResponseInfo.requestInfo.reason,510typingInterval: requestResponseInfo.requestInfo.typingInterval,511typingIntervalCharacterCount: requestResponseInfo.requestInfo.typingIntervalCharacterCount,512languageId: requestResponseInfo.requestInfo.languageId,513selectedSuggestionInfo: !!requestResponseInfo.context.selectedSuggestionInfo,514availableProviders: requestResponseInfo.providers.map(p => p.providerId?.toString()).filter(isDefined).join(','),515...forwardToChannelIf(requestResponseInfo.providers.some(p => isCopilotLikeExtension(p.providerId?.extensionId))),516timeUntilProviderRequest: undefined,517timeUntilProviderResponse: undefined,518viewKind: undefined,519preceeded: undefined,520superseded: undefined,521reason: undefined,522acceptedAlternativeAction: undefined,523correlationId: undefined,524shownDuration: undefined,525shownDurationUncollapsed: undefined,526timeUntilShown: undefined,527partiallyAccepted: undefined,528partiallyAcceptedCountSinceOriginal: undefined,529partiallyAcceptedRatioSinceOriginal: undefined,530partiallyAcceptedCharactersSinceOriginal: undefined,531cursorColumnDistance: undefined,532cursorLineDistance: undefined,533lineCountOriginal: undefined,534lineCountModified: undefined,535characterCountOriginal: undefined,536characterCountModified: undefined,537disjointReplacements: undefined,538sameShapeReplacements: undefined,539longDistanceHintVisible: undefined,540longDistanceHintDistance: undefined,541notShownReason: undefined,542renameCreated: false,543renameDuration: undefined,544renameTimedOut: false,545renameDroppedOtherEdits: undefined,546renameDroppedRenameEdits: undefined,547performanceMarkers: undefined,548editKind: undefined,549};550551const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService);552sendInlineCompletionsEndOfLifeTelemetry(dataChannel, emptyEndOfLifeEvent);553}554555public clearSuggestWidgetInlineCompletions(tx: ITransaction): void {556if (this._updateOperation.value?.request.context.selectedSuggestionInfo) {557this._updateOperation.clear();558}559}560561public cancelUpdate(): void {562this._updateOperation.clear();563}564}565566class UpdateRequest {567constructor(568public readonly position: Position,569public readonly context: InlineCompletionContextWithoutUuid,570public readonly versionId: number,571public readonly providers: Set<InlineCompletionsProvider>,572) {573}574575public satisfies(other: UpdateRequest): boolean {576return this.position.equals(other.position)577&& equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, thisEqualsC())578&& (other.context.triggerKind === InlineCompletionTriggerKind.Automatic579|| this.context.triggerKind === InlineCompletionTriggerKind.Explicit)580&& this.versionId === other.versionId581&& isSubset(other.providers, this.providers);582}583584public get isExplicitRequest() {585return this.context.triggerKind === InlineCompletionTriggerKind.Explicit;586}587}588589class RequestResponseData {590public requestUuid: string | undefined;591public noSuggestionReason: string | undefined;592public hasProducedSuggestion = false;593594constructor(595public readonly context: InlineCompletionContextWithoutUuid,596public readonly requestInfo: InlineSuggestRequestInfo,597public readonly providers: InlineCompletionsProvider[],598) { }599600setRequestUuid(uuid: string) {601this.requestUuid = uuid;602}603604setNoSuggestionReasonIfNotSet(type: string) {605this.noSuggestionReason ??= type;606}607608setHasProducedSuggestion() {609this.hasProducedSuggestion = true;610}611}612613function isSubset<T>(set1: Set<T>, set2: Set<T>): boolean {614return [...set1].every(item => set2.has(item));615}616617class UpdateOperation implements IDisposable {618constructor(619public readonly request: UpdateRequest,620public readonly cancellationTokenSource: CancellationTokenSource,621public readonly promise: Promise<boolean>,622) {623}624625dispose() {626this.cancellationTokenSource.cancel();627}628}629630export class InlineCompletionsState extends Disposable {631public static createEmpty(): InlineCompletionsState {632return new InlineCompletionsState([], undefined);633}634635constructor(636public readonly inlineCompletions: readonly InlineSuggestionItem[],637public readonly request: UpdateRequest | undefined,638) {639super();640641for (const inlineCompletion of this.inlineCompletions) {642inlineCompletion.addRef();643}644645this._register({646dispose: () => {647for (const inlineCompletion of this.inlineCompletions) {648inlineCompletion.removeRef();649}650}651});652}653654private _findById(id: InlineSuggestionIdentity): InlineSuggestionItem | undefined {655return this.inlineCompletions.find(i => i.identity === id);656}657658private _findByHash(hash: string): InlineSuggestionItem | undefined {659return this.inlineCompletions.find(i => i.hash === hash);660}661662/**663* Applies the edit on the state.664*/665public createStateWithAppliedEdit(edit: StringEdit, textModel: ITextModel): InlineCompletionsState {666const newInlineCompletions = this.inlineCompletions.map(i => i.withEdit(edit, textModel)).filter(isDefined);667return new InlineCompletionsState(newInlineCompletions, this.request);668}669670public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserveAtTop: InlineSuggestionIdentity | undefined): InlineCompletionsState {671let itemToPreserve: InlineSuggestionItem | undefined = undefined;672if (itemIdToPreserveAtTop) {673const itemToPreserveCandidate = this._findById(itemIdToPreserveAtTop);674if (itemToPreserveCandidate && itemToPreserveCandidate.canBeReused(textModel, request.position)) {675itemToPreserve = itemToPreserveCandidate;676677const updatedItemToPreserve = updatedSuggestions.find(i => i.hash === itemToPreserveCandidate.hash);678if (updatedItemToPreserve) {679updatedSuggestions = moveToFront(updatedItemToPreserve, updatedSuggestions);680} else {681updatedSuggestions = [itemToPreserveCandidate, ...updatedSuggestions];682}683}684}685686const preferInlineCompletions = itemToPreserve687// itemToPreserve has precedence688? !itemToPreserve.isInlineEdit689// Otherwise: prefer inline completion if there is a visible one690: updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition));691692let updatedItems: InlineSuggestionItem[] = [];693for (const i of updatedSuggestions) {694const oldItem = this._findByHash(i.hash);695let item;696if (oldItem && oldItem !== i) {697item = i.withIdentity(oldItem.identity);698i.setIsPreceeded(oldItem);699oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() });700} else {701item = i;702}703if (preferInlineCompletions !== item.isInlineEdit) {704updatedItems.push(item);705}706}707708updatedItems.sort(compareBy(i => i.showInlineEditMenu, booleanComparator));709updatedItems = distinctByKey(updatedItems, i => i.semanticId);710711return new InlineCompletionsState(updatedItems, request);712}713714public clone(): InlineCompletionsState {715return new InlineCompletionsState(this.inlineCompletions, this.request);716}717}718719/** Keeps the first item in case of duplicates. */720function distinctByKey<T>(items: T[], key: (item: T) => unknown): T[] {721const seen = new Set();722return items.filter(item => {723const k = key(item);724if (seen.has(k)) {725return false;726}727seen.add(k);728return true;729});730}731732function moveToFront<T>(item: T, items: T[]): T[] {733const index = items.indexOf(item);734if (index > -1) {735return [item, ...items.slice(0, index), ...items.slice(index + 1)];736}737return items;738}739740741