Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts
5272 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 { mapFindFirst } from '../../../../../base/common/arraysFind.js';6import { arrayEqualsC } from '../../../../../base/common/equals.js';7import { BugIndicatingError, onUnexpectedExternalError } from '../../../../../base/common/errors.js';8import { Emitter } from '../../../../../base/common/event.js';9import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';10import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, constObservable, derived, derivedHandleChanges, derivedOpts, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js';11import { firstNonWhitespaceIndex } from '../../../../../base/common/strings.js';12import { isDefined } from '../../../../../base/common/types.js';13import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';14import { ICommandService } from '../../../../../platform/commands/common/commands.js';15import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';16import { ICodeEditor } from '../../../../browser/editorBrowser.js';17import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js';18import { EditorOption } from '../../../../common/config/editorOptions.js';19import { CursorColumns } from '../../../../common/core/cursorColumns.js';20import { LineRange } from '../../../../common/core/ranges/lineRange.js';21import { Position } from '../../../../common/core/position.js';22import { Range } from '../../../../common/core/range.js';23import { Selection } from '../../../../common/core/selection.js';24import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js';25import { TextLength } from '../../../../common/core/text/textLength.js';26import { ScrollType } from '../../../../common/editorCommon.js';27import { IInlineCompletionChangeHint, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider, InlineCompletionCommand } from '../../../../common/languages.js';28import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';29import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js';30import { TextModelText } from '../../../../common/model/textModelText.js';31import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';32import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js';33import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';34import { SnippetController2 } from '../../../snippet/browser/snippetController2.js';35import { getEndPositionsAfterApplying, removeTextReplacementCommonSuffixPrefix } from '../utils.js';36import { AnimatedValue, easeOutCubic, ObservableAnimatedValue } from './animation.js';37import { computeGhostText } from './computeGhostText.js';38import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js';39import { InlineCompletionsSource } from './inlineCompletionsSource.js';40import { InlineCompletionItem, InlineEditItem, InlineSuggestionItem } from './inlineSuggestionItem.js';41import { InlineCompletionContextWithoutUuid, InlineCompletionEditorType, InlineSuggestRequestInfo, InlineSuggestSku } from './provideInlineCompletions.js';42import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';43import { SuggestItemInfo } from './suggestWidgetAdapter.js';44import { TextModelEditSource, EditSources } from '../../../../common/textModelEditSource.js';45import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js';46import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';47import { IInlineCompletionsService } from '../../../../browser/services/inlineCompletionsService.js';48import { TypingInterval } from './typingSpeed.js';49import { StringReplacement } from '../../../../common/core/edits/stringEdit.js';50import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';51import { URI } from '../../../../../base/common/uri.js';52import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';53import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js';54import { Schemas } from '../../../../../base/common/network.js';55import { getInlineCompletionsController } from '../controller/common.js';5657export class InlineCompletionsModel extends Disposable {58private readonly _source;59private readonly _isActive = observableValue<boolean>(this, false);60private readonly _onlyRequestInlineEditsSignal = observableSignal(this);61private readonly _forceUpdateExplicitlySignal = observableSignal(this);62private readonly _noDelaySignal = observableSignal(this);6364private readonly _fetchSpecificProviderSignal = observableSignal<{ provider: InlineCompletionsProvider; changeHint?: IInlineCompletionChangeHint } | undefined>(this);6566// We use a semantic id to keep the same inline completion selected even if the provider reorders the completions.67private readonly _selectedInlineCompletionId = observableValue<string | undefined>(this, undefined);68public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1));69public readonly allPositions = derived(this, reader => this._positions.read(reader));7071private readonly sku = observableValue<InlineSuggestSku | undefined>(this, undefined);7273private _isAcceptingPartially = false;74private readonly _appearedInsideViewport = derived<boolean>(this, reader => {75const state = this.state.read(reader);76if (!state || !state.inlineSuggestion) {77return false;78}7980return isSuggestionInViewport(this._editor, state.inlineSuggestion);81});82public get isAcceptingPartially() { return this._isAcceptingPartially; }8384private readonly _onDidAccept = this._register(new Emitter<void>());85public readonly onDidAccept = this._onDidAccept.event;8687private readonly _editorObs;8889private readonly _typing: TypingInterval;9091private readonly _suggestPreviewEnabled;92private readonly _suggestPreviewMode;93private readonly _inlineSuggestMode;94private readonly _suppressedInlineCompletionGroupIds;95private readonly _inlineEditsEnabled;96private readonly _inlineEditsShowCollapsedEnabled;97private readonly _triggerCommandOnProviderChange;98private readonly _minShowDelay;99private readonly _showOnSuggestConflict;100private readonly _suppressInSnippetMode;101private readonly _isInSnippetMode;102103get editor() {104return this._editor;105}106107constructor(108public readonly textModel: ITextModel,109private readonly _selectedSuggestItem: IObservable<SuggestItemInfo | undefined>,110public readonly _textModelVersionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>,111private readonly _positions: IObservable<readonly Position[]>,112private readonly _debounceValue: IFeatureDebounceInformation,113private readonly _enabled: IObservable<boolean>,114private readonly _editor: ICodeEditor,115@IInstantiationService private readonly _instantiationService: IInstantiationService,116@ICommandService private readonly _commandService: ICommandService,117@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,118@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,119@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,120@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,121@IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService,122@IDefaultAccountService defaultAccountService: IDefaultAccountService,123) {124super();125this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue, this.primaryPosition));126this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind);127128this._editorObs = observableCodeEditor(this._editor);129130const suggest = this._editorObs.getOption(EditorOption.suggest);131this._suggestPreviewEnabled = suggest.map(v => v.preview);132this._suggestPreviewMode = suggest.map(v => v.previewMode);133134const inlineSuggest = this._editorObs.getOption(EditorOption.inlineSuggest);135this._inlineSuggestMode = inlineSuggest.map(v => v.mode);136this._suppressedInlineCompletionGroupIds = inlineSuggest.map(v => new Set(v.experimental.suppressInlineSuggestions.split(',')));137this._inlineEditsEnabled = inlineSuggest.map(v => !!v.edits.enabled);138this._inlineEditsShowCollapsedEnabled = inlineSuggest.map(s => s.edits.showCollapsed);139this._triggerCommandOnProviderChange = inlineSuggest.map(s => s.triggerCommandOnProviderChange);140this._minShowDelay = inlineSuggest.map(s => s.minShowDelay);141this._showOnSuggestConflict = inlineSuggest.map(s => s.experimental.showOnSuggestConflict);142this._suppressInSnippetMode = inlineSuggest.map(s => s.suppressInSnippetMode);143144const snippetController = SnippetController2.get(this._editor);145this._isInSnippetMode = snippetController?.isInSnippetObservable ?? constObservable(false);146147defaultAccountService.getDefaultAccount().then(createDisposableCb(account => this.sku.set(skuFromAccount(account), undefined), this._store));148this._register(defaultAccountService.onDidChangeDefaultAccount(account => this.sku.set(skuFromAccount(account), undefined)));149150this._typing = this._register(new TypingInterval(this.textModel));151152this._register(this._inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => {153if (isSnoozing) {154this.stop();155}156}));157158{ // Determine editor type159const isNotebook = this.textModel.uri.scheme === Schemas.vscodeNotebookCell;160const [diffEditor] = this._codeEditorService.listDiffEditors()161.filter(d =>162d.getOriginalEditor().getId() === this._editor.getId() ||163d.getModifiedEditor().getId() === this._editor.getId());164165this.isInDiffEditor = !!diffEditor;166this.editorType = isNotebook ? InlineCompletionEditorType.Notebook167: this.isInDiffEditor ? InlineCompletionEditorType.DiffEditor168: InlineCompletionEditorType.TextEditor;169}170171this._register(recomputeInitiallyAndOnChange(this.state, (s) => {172if (s && s.inlineSuggestion) {173this._inlineCompletionsService.reportNewCompletion(s.inlineSuggestion.requestUuid);174}175}));176177this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise));178179this._register(autorun(reader => {180this._editorObs.versionId.read(reader);181this._inAcceptFlow.set(false, undefined);182}));183184this._register(autorun(reader => {185const jumpToReset = this.state.map((s, reader) => !s || s.kind === 'inlineEdit' && !s.cursorAtInlineEdit.read(reader)).read(reader);186if (jumpToReset) {187this._jumpedToId.set(undefined, undefined);188}189}));190191this._register(autorun(reader => {192const inlineSuggestion = this.state.map(s => s?.inlineSuggestion).read(reader);193if (inlineSuggestion) {194inlineSuggestion.addPerformanceMarker('activeSuggestion');195}196}));197198const inlineEditSemanticId = this.inlineEditState.map(s => s?.inlineSuggestion.semanticId);199200this._register(autorun(reader => {201const id = inlineEditSemanticId.read(reader);202if (id) {203this._editor.pushUndoStop();204this._lastShownInlineCompletionInfo = {205alternateTextModelVersionId: this.textModel.getAlternativeVersionId(),206inlineCompletion: this.state.get()!.inlineSuggestion!,207};208}209}));210211// TODO: should use getAvailableProviders and update on _suppressedInlineCompletionGroupIds change212const inlineCompletionProviders = observableFromEvent(this._languageFeaturesService.inlineCompletionsProvider.onDidChange, () => this._languageFeaturesService.inlineCompletionsProvider.all(textModel));213mapObservableArrayCached(this, inlineCompletionProviders, (provider, store) => {214if (!provider.onDidChangeInlineCompletions) {215return;216}217218store.add(provider.onDidChangeInlineCompletions(changeHint => {219if (!this._enabled.get()) {220return;221}222223// Only update the active editor224const activeEditor = this._codeEditorService.getFocusedCodeEditor() || this._codeEditorService.getActiveCodeEditor();225if (activeEditor !== this._editor) {226return;227}228229if (this._triggerCommandOnProviderChange.get()) {230// TODO@hediet remove this and always do the else branch.231this.trigger(undefined, { onlyFetchInlineEdits: true });232return;233}234235236// If there is an active suggestion from a different provider, we ignore the update237const activeState = this.state.get();238if (activeState && (activeState.inlineSuggestion || activeState.edits) && activeState.inlineSuggestion?.source.provider !== provider) {239return;240}241242transaction(tx => {243this._fetchSpecificProviderSignal.trigger(tx, { provider, changeHint: changeHint ?? undefined });244this.trigger(tx);245});246247}));248}).recomputeInitiallyAndOnChange(this._store);249250this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store);251}252253private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined;254private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined;255private readonly _didUndoInlineEdits = derivedHandleChanges({256owner: this,257changeTracker: {258createChangeSummary: () => ({ didUndo: false }),259handleChange: (ctx, changeSummary) => {260changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing;261return true;262}263}264}, (reader, changeSummary) => {265const versionId = this._textModelVersionId.read(reader);266if (versionId !== null267&& this._lastAcceptedInlineCompletionInfo268&& this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1269&& this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit270&& changeSummary.didUndo271) {272this._lastAcceptedInlineCompletionInfo = undefined;273return true;274}275return false;276});277278public debugGetSelectedSuggestItem(): IObservable<SuggestItemInfo | undefined> {279return this._selectedSuggestItem;280}281282public getIndentationInfo(reader: IReader) {283let startsWithIndentation = false;284let startsWithIndentationLessThanTabSize = true;285const ghostText = this?.primaryGhostText.read(reader);286if (!!this?._selectedSuggestItem && ghostText && ghostText.parts.length > 0) {287const { column, lines } = ghostText.parts[0];288289const firstLine = lines[0].line;290291const indentationEndColumn = this.textModel.getLineIndentColumn(ghostText.lineNumber);292const inIndentation = column <= indentationEndColumn;293294if (inIndentation) {295let firstNonWsIdx = firstNonWhitespaceIndex(firstLine);296if (firstNonWsIdx === -1) {297firstNonWsIdx = firstLine.length - 1;298}299startsWithIndentation = firstNonWsIdx > 0;300301const tabSize = this.textModel.getOptions().tabSize;302const visibleColumnIndentation = CursorColumns.visibleColumnFromColumn(firstLine, firstNonWsIdx + 1, tabSize);303startsWithIndentationLessThanTabSize = visibleColumnIndentation < tabSize;304}305}306return {307startsWithIndentation,308startsWithIndentationLessThanTabSize,309};310}311312private readonly _preserveCurrentCompletionReasons = new Set([313VersionIdChangeReason.Redo,314VersionIdChangeReason.Undo,315VersionIdChangeReason.AcceptWord,316]);317318private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason {319if (e?.isUndoing) { return VersionIdChangeReason.Undo; }320if (e?.isRedoing) { return VersionIdChangeReason.Redo; }321if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; }322return VersionIdChangeReason.Other;323}324325public readonly dontRefetchSignal = observableSignal(this);326327private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({328owner: this,329changeTracker: {330createChangeSummary: () => ({331dontRefetch: false,332preserveCurrentCompletion: false,333inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic,334onlyRequestInlineEdits: false,335shouldDebounce: true,336provider: undefined as InlineCompletionsProvider | undefined,337changeHint: undefined as IInlineCompletionChangeHint | undefined,338textChange: false,339changeReason: '',340}),341handleChange: (ctx, changeSummary) => {342/** @description fetch inline completions */343if (ctx.didChange(this._textModelVersionId)) {344if (this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) {345changeSummary.preserveCurrentCompletion = true;346}347const detailedReasons = ctx.change?.detailedReasons ?? [];348changeSummary.changeReason = detailedReasons.length > 0 ? detailedReasons[0].getType() : '';349changeSummary.textChange = true;350} else if (ctx.didChange(this._forceUpdateExplicitlySignal)) {351changeSummary.preserveCurrentCompletion = true;352changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit;353} else if (ctx.didChange(this.dontRefetchSignal)) {354changeSummary.dontRefetch = true;355} else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) {356changeSummary.onlyRequestInlineEdits = true;357} else if (ctx.didChange(this._fetchSpecificProviderSignal)) {358changeSummary.provider = ctx.change?.provider;359changeSummary.changeHint = ctx.change?.changeHint;360}361return true;362},363},364365}, (reader, changeSummary) => {366this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation367this._noDelaySignal.read(reader);368this.dontRefetchSignal.read(reader);369this._onlyRequestInlineEditsSignal.read(reader);370this._forceUpdateExplicitlySignal.read(reader);371this._fetchSpecificProviderSignal.read(reader);372const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader))373&& (!this._inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit);374if (!shouldUpdate) {375this._source.cancelUpdate();376return undefined;377}378379this._textModelVersionId.read(reader); // Refetch on text change380381const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(undefined);382let suggestItem = this._selectedSuggestItem.read(reader);383if (this._shouldShowOnSuggestConflict.read(undefined)) {384suggestItem = undefined;385}386if (suggestWidgetInlineCompletions && !suggestItem) {387this._source.seedInlineCompletionsWithSuggestWidget();388}389390if (changeSummary.dontRefetch) {391return Promise.resolve(true);392}393394if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) {395transaction(tx => {396this._source.clear(tx);397});398return undefined;399}400401let reason: string = '';402if (changeSummary.provider) {403reason += 'providerOnDidChange';404} else if (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit) {405reason += 'explicit';406}407if (changeSummary.changeReason) {408reason += reason.length > 0 ? `:${changeSummary.changeReason}` : changeSummary.changeReason;409}410411const typingInterval = this._typing.getTypingInterval();412const requestInfo: InlineSuggestRequestInfo = {413editorType: this.editorType,414startTime: Date.now(),415languageId: this.textModel.getLanguageId(),416reason,417typingInterval: typingInterval.averageInterval,418typingIntervalCharacterCount: typingInterval.characterCount,419availableProviders: [],420sku: this.sku.read(undefined),421};422423let context: InlineCompletionContextWithoutUuid = {424triggerKind: changeSummary.inlineCompletionTriggerKind,425selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(),426includeInlineCompletions: !changeSummary.onlyRequestInlineEdits,427includeInlineEdits: this._inlineEditsEnabled.read(reader),428requestIssuedDateTime: requestInfo.startTime,429earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.read(undefined) ? 0 : this._minShowDelay.read(undefined)),430changeHint: changeSummary.changeHint,431};432433if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) {434if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) {435// When undoing back to a version where an inline edit/completion was shown,436// we want to show an inline edit (or completion) again if it was originally an inline edit (or completion).437context = {438...context,439includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit,440includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit,441};442}443}444445const itemToPreserveCandidate = this.selectedInlineCompletion.read(undefined) ?? this._inlineCompletionItems.read(undefined)?.inlineEdit;446const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable447? itemToPreserveCandidate : undefined;448const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.read(undefined)?.inlineEdit?.semanticId);449450const providers = changeSummary.provider451? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId?.toString() }452: { providers: this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel), label: undefined }; // TODO: should use inlineCompletionProviders453const availableProviders = this.getAvailableProviders(providers.providers);454requestInfo.availableProviders = availableProviders.map(p => p.providerId).filter(isDefined);455456return this._source.fetch(availableProviders, providers.label, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, requestInfo);457});458459// TODO: This is not an ideal implementation of excludesGroupIds, however as this is currently still behind proposed API460// and due to the time constraints, we are using a simplified approach461private getAvailableProviders(providers: InlineCompletionsProvider[]): InlineCompletionsProvider[] {462const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get();463const unsuppressedProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId)));464465const excludedGroupIds = new Set<string>();466for (const provider of unsuppressedProviders) {467provider.excludesGroupIds?.forEach(p => excludedGroupIds.add(p));468}469470const availableProviders: InlineCompletionsProvider[] = [];471for (const provider of unsuppressedProviders) {472if (provider.groupId && excludedGroupIds.has(provider.groupId)) {473continue;474}475availableProviders.push(provider);476}477478return availableProviders;479}480481public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean; changeHint?: IInlineCompletionChangeHint } = {}): Promise<void> {482subtransaction(tx, tx => {483if (options.onlyFetchInlineEdits) {484this._onlyRequestInlineEditsSignal.trigger(tx);485}486if (options.noDelay) {487this._noDelaySignal.trigger(tx);488}489this._isActive.set(true, tx);490491if (options.explicit) {492this._inAcceptFlow.set(true, tx);493this._forceUpdateExplicitlySignal.trigger(tx);494}495if (options.provider) {496this._fetchSpecificProviderSignal.trigger(tx, { provider: options.provider, changeHint: options.changeHint });497}498});499await this._fetchInlineCompletionsPromise.get();500}501502public async triggerExplicitly(tx?: ITransaction, onlyFetchInlineEdits: boolean = false): Promise<void> {503return this.trigger(tx, { onlyFetchInlineEdits, explicit: true });504}505506public stop(stopReason: 'explicitCancel' | 'automatic' = 'automatic', tx?: ITransaction): void {507subtransaction(tx, tx => {508if (stopReason === 'explicitCancel') {509const inlineCompletion = this.state.get()?.inlineSuggestion;510if (inlineCompletion) {511inlineCompletion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Rejected });512}513}514515this._isActive.set(false, tx);516this._source.clear(tx);517});518}519520private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => {521const c = this._source.inlineCompletions.read(reader);522if (!c) { return undefined; }523const cursorPosition = this.primaryPosition.read(reader);524let inlineEdit: InlineEditItem | undefined = undefined;525const visibleCompletions: InlineCompletionItem[] = [];526for (const completion of c.inlineCompletions) {527if (!completion.isInlineEdit) {528if (completion.isVisible(this.textModel, cursorPosition)) {529visibleCompletions.push(completion);530}531} else {532inlineEdit = completion;533}534}535536if (visibleCompletions.length !== 0) {537// Don't show the inline edit if there is a visible completion538inlineEdit = undefined;539}540541return {542inlineCompletions: visibleCompletions,543inlineEdit,544};545});546547private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: arrayEqualsC() }, reader => {548const c = this._inlineCompletionItems.read(reader);549return c?.inlineCompletions ?? [];550});551552public readonly selectedInlineCompletionIndex = derived<number>(this, (reader) => {553const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader);554const filteredCompletions = this._filteredInlineCompletionItems.read(reader);555const idx = this._selectedInlineCompletionId === undefined ? -1556: filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId);557if (idx === -1) {558// Reset the selection so that the selection does not jump back when it appears again559this._selectedInlineCompletionId.set(undefined, undefined);560return 0;561}562return idx;563});564565public readonly selectedInlineCompletion = derived<InlineCompletionItem | undefined>(this, (reader) => {566const filteredCompletions = this._filteredInlineCompletionItems.read(reader);567const idx = this.selectedInlineCompletionIndex.read(reader);568return filteredCompletions[idx];569});570571public readonly activeCommands = derivedOpts<InlineCompletionCommand[]>({ owner: this, equalsFn: arrayEqualsC() },572r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? []573);574575public readonly lastTriggerKind: IObservable<InlineCompletionTriggerKind | undefined>;576577public readonly inlineCompletionsCount = derived<number | undefined>(this, reader => {578if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) {579return this._filteredInlineCompletionItems.read(reader).length;580} else {581return undefined;582}583});584585private readonly _hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0);586587private readonly _shouldShowOnSuggestConflict = derived(this, reader => {588const showOnSuggestConflict = this._showOnSuggestConflict.read(reader);589if (showOnSuggestConflict !== 'never') {590const hasInlineCompletion = !!this.selectedInlineCompletion.read(reader);591if (hasInlineCompletion) {592const item = this._selectedSuggestItem.read(reader);593if (!item) {594return false;595}596if (showOnSuggestConflict === 'whenSuggestListIsIncomplete') {597return item.listIncomplete;598}599return true;600}601}602return false;603});604605public readonly state = derivedOpts<{606kind: 'ghostText';607edits: readonly TextReplacement[];608primaryGhostText: GhostTextOrReplacement;609ghostTexts: readonly GhostTextOrReplacement[];610suggestItem: SuggestItemInfo | undefined;611inlineSuggestion: InlineCompletionItem | undefined;612} | {613kind: 'inlineEdit';614edits: readonly TextReplacement[];615inlineSuggestion: InlineEditItem;616cursorAtInlineEdit: IObservable<boolean>;617nextEditUri: URI | undefined;618} | undefined>({619owner: this,620equalsFn: (a, b) => {621if (!a || !b) { return a === b; }622623if (a.kind === 'ghostText' && b.kind === 'ghostText') {624return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts)625&& a.inlineSuggestion === b.inlineSuggestion626&& a.suggestItem === b.suggestItem;627} else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') {628return a.inlineSuggestion === b.inlineSuggestion;629}630return false;631}632}, (reader) => {633const model = this.textModel;634635if (this._suppressInSnippetMode.read(reader) && this._isInSnippetMode.read(reader)) {636return undefined;637}638639const item = this._inlineCompletionItems.read(reader);640const inlineEditResult = item?.inlineEdit;641if (inlineEditResult) {642if (this._hasVisiblePeekWidgets.read(reader)) {643return undefined;644}645const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber));646const stringEdit = inlineEditResult.action?.kind === 'edit' ? inlineEditResult.action.stringEdit : undefined;647const replacements = stringEdit ? TextEdit.fromStringEdit(stringEdit, new TextModelText(this.textModel)).replacements : [];648649let nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') &&650// eslint-disable-next-line local/code-no-any-casts651item.inlineEdit?.command.arguments?.length ? URI.from(<any>item.inlineEdit?.command.arguments[0]) : undefined;652if (!inlineEditResult.originalTextRef.targets(this.textModel)) {653nextEditUri = inlineEditResult.originalTextRef.uri;654}655return { kind: 'inlineEdit', inlineSuggestion: inlineEditResult, edits: replacements, cursorAtInlineEdit, nextEditUri };656}657658const suggestItem = this._selectedSuggestItem.read(reader);659if (!this._shouldShowOnSuggestConflict.read(reader) && suggestItem) {660const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model);661const augmentation = this._computeAugmentation(suggestCompletionEdit, reader);662663const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader);664if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; }665666const fullEdit = augmentation?.edit ?? suggestCompletionEdit;667const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0;668669const mode = this._suggestPreviewMode.read(reader);670const positions = this._positions.read(reader);671const allPotentialEdits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)];672const validEditsAndGhostTexts = allPotentialEdits673.map((edit, idx) => ({ edit, ghostText: edit ? computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength) : undefined }))674.filter(({ edit, ghostText }) => edit !== undefined && ghostText !== undefined);675const edits = validEditsAndGhostTexts.map(({ edit }) => edit!);676const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!);677const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []);678return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineSuggestion: augmentation?.completion, suggestItem };679} else {680if (!this._isActive.read(reader)) { return undefined; }681const inlineSuggestion = this.selectedInlineCompletion.read(reader);682if (!inlineSuggestion) { return undefined; }683684const replacement = inlineSuggestion.getSingleTextEdit();685const mode = this._inlineSuggestMode.read(reader);686const positions = this._positions.read(reader);687const allPotentialEdits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)];688const validEditsAndGhostTexts = allPotentialEdits689.map((edit, idx) => ({ edit, ghostText: edit ? computeGhostText(edit, model, mode, positions[idx], 0) : undefined }))690.filter(({ edit, ghostText }) => edit !== undefined && ghostText !== undefined);691const edits = validEditsAndGhostTexts.map(({ edit }) => edit!);692const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!);693if (!ghostTexts[0]) { return undefined; }694return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineSuggestion, suggestItem: undefined };695}696});697698public readonly status = derived(this, reader => {699if (this._source.loading.read(reader)) { return 'loading'; }700const s = this.state.read(reader);701if (s?.kind === 'ghostText') { return 'ghostText'; }702if (s?.kind === 'inlineEdit') { return 'inlineEdit'; }703return 'noSuggestion';704});705706public readonly inlineCompletionState = derived(this, reader => {707const s = this.state.read(reader);708if (!s || s.kind !== 'ghostText') {709return undefined;710}711if (this._editorObs.inComposition.read(reader)) {712return undefined;713}714return s;715});716717public readonly inlineEditState = derived(this, reader => {718const s = this.state.read(reader);719if (!s || s.kind !== 'inlineEdit') {720return undefined;721}722return s;723});724725public readonly inlineEditAvailable = derived(this, reader => {726const s = this.inlineEditState.read(reader);727return !!s;728});729730private _computeAugmentation(suggestCompletion: TextReplacement, reader: IReader | undefined) {731const model = this.textModel;732const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(reader);733const candidateInlineCompletions = suggestWidgetInlineCompletions734? suggestWidgetInlineCompletions.inlineCompletions.filter(c => !c.isInlineEdit)735: [this.selectedInlineCompletion.read(reader)].filter(isDefined);736737const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => {738let r = completion.getSingleTextEdit();739r = singleTextRemoveCommonPrefix(740r,741model,742Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())743);744return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined;745});746747return augmentedCompletion;748}749750public readonly warning = derived(this, reader => {751return this.inlineCompletionState.read(reader)?.inlineSuggestion?.warning;752});753754public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => {755const v = this.inlineCompletionState.read(reader);756if (!v) {757return undefined;758}759return v.ghostTexts;760});761762public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => {763const v = this.inlineCompletionState.read(reader);764if (!v) {765return undefined;766}767return v?.primaryGhostText;768});769770public readonly showCollapsed = derived<boolean>(this, reader => {771const state = this.state.read(reader);772if (!state || state.kind !== 'inlineEdit') {773return false;774}775776if (state.inlineSuggestion.hint || state.inlineSuggestion.action?.kind === 'jumpTo') {777return false;778}779780const isCurrentModelVersion = state.inlineSuggestion.updatedEditModelVersion === this._textModelVersionId.read(reader);781return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion)782&& this._jumpedToId.read(reader) !== state.inlineSuggestion.semanticId783&& !this._inAcceptFlow.read(reader);784});785786private readonly _tabShouldIndent = derived(this, reader => {787if (this._inAcceptFlow.read(reader)) {788return false;789}790791function isMultiLine(range: Range): boolean {792return range.startLineNumber !== range.endLineNumber;793}794795function getNonIndentationRange(model: ITextModel, lineNumber: number): Range {796const columnStart = model.getLineIndentColumn(lineNumber);797const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber);798const columnEnd = Math.max(lastNonWsColumn, columnStart);799return new Range(lineNumber, columnStart, lineNumber, columnEnd);800}801802const selections = this._editorObs.selections.read(reader);803return selections?.some(s => {804if (s.isEmpty()) {805return this.textModel.getLineLength(s.startLineNumber) === 0;806} else {807return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber));808}809});810});811812public readonly tabShouldJumpToInlineEdit = derived(this, reader => {813if (this._tabShouldIndent.read(reader)) {814return false;815}816817const s = this.inlineEditState.read(reader);818if (!s) {819return false;820}821822823if (s.inlineSuggestion.action?.kind === 'jumpTo') {824return true;825}826827if (this.showCollapsed.read(reader)) {828return true;829}830831if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) {832return false;833}834835return !s.cursorAtInlineEdit.read(reader);836});837838public readonly tabShouldAcceptInlineEdit = derived(this, reader => {839const s = this.inlineEditState.read(reader);840if (!s) {841return false;842}843if (s.inlineSuggestion.action?.kind === 'jumpTo') {844return false;845}846if (this.showCollapsed.read(reader)) {847return false;848}849if (this._tabShouldIndent.read(reader)) {850return false;851}852if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) {853return true;854}855if (s.inlineSuggestion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) {856return true;857}858if (this._jumpedToId.read(reader) === s.inlineSuggestion.semanticId) {859return true;860}861862return s.cursorAtInlineEdit.read(reader);863});864865public readonly isInDiffEditor;866867public readonly editorType: InlineCompletionEditorType;868869private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise<void> {870await this.triggerExplicitly();871872const completions = this._filteredInlineCompletionItems.get() || [];873if (completions.length > 0) {874const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length;875this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined);876} else {877this._selectedInlineCompletionId.set(undefined, undefined);878}879}880881public async next(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(1); }882883public async previous(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(-1); }884885private _getMetadata(completion: InlineSuggestionItem, languageId: string, type: 'word' | 'line' | undefined = undefined): TextModelEditSource {886if (type) {887return EditSources.inlineCompletionPartialAccept({888nes: completion.isInlineEdit,889requestUuid: completion.requestUuid,890providerId: completion.source.provider.providerId,891languageId,892type,893correlationId: completion.getSourceCompletion().correlationId,894});895} else {896return EditSources.inlineCompletionAccept({897nes: completion.isInlineEdit,898requestUuid: completion.requestUuid,899correlationId: completion.getSourceCompletion().correlationId,900providerId: completion.source.provider.providerId,901languageId902});903}904}905906public async accept(editor: ICodeEditor = this._editor, alternativeAction: boolean = false): Promise<void> {907if (editor.getModel() !== this.textModel) {908throw new BugIndicatingError();909}910911let completion: InlineSuggestionItem;912let isNextEditUri = false;913const state = this.state.get();914if (state?.kind === 'ghostText') {915if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) {916return;917}918completion = state.inlineSuggestion;919} else if (state?.kind === 'inlineEdit') {920completion = state.inlineSuggestion;921isNextEditUri = !!state.nextEditUri;922} else {923return;924}925926// Make sure the completion list will not be disposed before the text change is sent.927completion.addRef();928929try {930let followUpTrigger = false;931editor.pushUndoStop();932933if (!completion.originalTextRef.targets(this.textModel)) {934// The edit targets a different document, open it and transplant the completion935const targetEditor = await this._codeEditorService.openCodeEditor({ resource: completion.originalTextRef.uri }, this._editor);936if (targetEditor) {937const controller = getInlineCompletionsController(targetEditor);938const m = controller?.model.get();939targetEditor.focus();940m?.transplantCompletion(completion);941targetEditor.revealLineInCenter(completion.targetRange.startLineNumber);942}943} else if (isNextEditUri) {944// Do nothing945} else if (completion.action?.kind === 'edit') {946const action = completion.action;947if (alternativeAction && action.alternativeAction) {948followUpTrigger = true;949const altCommand = action.alternativeAction.command;950await this._commandService951.executeCommand(altCommand.id, ...(altCommand.arguments || []))952.then(undefined, onUnexpectedExternalError);953} else if (action.snippetInfo) {954const mainEdit = TextReplacement.delete(action.textReplacement.range);955const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));956const edit = TextEdit.fromParallelReplacementsUnsorted([mainEdit, ...additionalEdits]);957editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId()));958959editor.setPosition(action.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept');960SnippetController2.get(editor)?.insert(action.snippetInfo.snippet, { undoStopBefore: false });961} else {962const edits = state.edits;963964// The cursor should move to the end of the edit, not the end of the range provided by the extension965// Inline Edit diffs (human readable) the suggestion from the extension so it already removes common suffix/prefix966// Inline Completions does diff the suggestion so it may contain common suffix967let minimalEdits = edits;968if (state.kind === 'ghostText') {969minimalEdits = removeTextReplacementCommonSuffixPrefix(edits, this.textModel);970}971const selections = getEndPositionsAfterApplying(minimalEdits).map(p => Selection.fromPositions(p));972973const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));974const edit = TextEdit.fromParallelReplacementsUnsorted([...edits, ...additionalEdits]);975976editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId()));977978if (completion.hint === undefined) {979// do not move the cursor when the completion is displayed in a different location980editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept');981}982983if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) {984const editRanges = edit.getNewRanges();985const dec = this._store.add(new FadeoutDecoration(editor, editRanges, () => {986this._store.delete(dec);987}));988}989}990}991992this._onDidAccept.fire();993994// Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset).995this.stop();996997if (completion.command) {998await this._commandService999.executeCommand(completion.command.id, ...(completion.command.arguments || []))1000.then(undefined, onUnexpectedExternalError);1001}10021003// TODO: how can we make alternative actions to retrigger?1004if (followUpTrigger) {1005this.trigger(undefined);1006}10071008completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction });1009} finally {1010completion.removeRef();1011this._inAcceptFlow.set(true, undefined);1012this._lastAcceptedInlineCompletionInfo = { textModelVersionIdAfter: this.textModel.getVersionId(), inlineCompletion: completion };1013}1014}10151016public async acceptNextWord(): Promise<void> {1017await this._acceptNext(this._editor, 'word', (pos, text) => {1018const langId = this.textModel.getLanguageIdAtPosition(pos.lineNumber, pos.column);1019const config = this._languageConfigurationService.getLanguageConfiguration(langId);1020const wordRegExp = new RegExp(config.wordDefinition.source, config.wordDefinition.flags.replace('g', ''));10211022const m1 = text.match(wordRegExp);1023let acceptUntilIndexExclusive = 0;1024if (m1 && m1.index !== undefined) {1025if (m1.index === 0) {1026acceptUntilIndexExclusive = m1[0].length;1027} else {1028acceptUntilIndexExclusive = m1.index;1029}1030} else {1031acceptUntilIndexExclusive = text.length;1032}10331034const wsRegExp = /\s+/g;1035const m2 = wsRegExp.exec(text);1036if (m2 && m2.index !== undefined) {1037if (m2.index + m2[0].length < acceptUntilIndexExclusive) {1038acceptUntilIndexExclusive = m2.index + m2[0].length;1039}1040}1041return acceptUntilIndexExclusive;1042}, PartialAcceptTriggerKind.Word);1043}10441045public async acceptNextLine(): Promise<void> {1046await this._acceptNext(this._editor, 'line', (pos, text) => {1047const m = text.match(/\n/);1048if (m && m.index !== undefined) {1049return m.index + 1;1050}1051return text.length;1052}, PartialAcceptTriggerKind.Line);1053}10541055private async _acceptNext(editor: ICodeEditor, type: 'word' | 'line', getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise<void> {1056if (editor.getModel() !== this.textModel) {1057throw new BugIndicatingError();1058}10591060const state = this.inlineCompletionState.get();1061if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) {1062return;1063}1064const ghostText = state.primaryGhostText;1065const completion = state.inlineSuggestion;10661067if (completion.snippetInfo) {1068// not in WYSIWYG mode, partial commit might change completion, thus it is not supported1069await this.accept(editor);1070return;1071}10721073const firstPart = ghostText.parts[0];1074const ghostTextPos = new Position(ghostText.lineNumber, firstPart.column);1075const ghostTextVal = firstPart.text;1076const acceptUntilIndexExclusive = getAcceptUntilIndex(ghostTextPos, ghostTextVal);1077if (acceptUntilIndexExclusive === ghostTextVal.length && ghostText.parts.length === 1) {1078this.accept(editor);1079return;1080}1081const partialGhostTextVal = ghostTextVal.substring(0, acceptUntilIndexExclusive);10821083const positions = this._positions.get();1084const cursorPosition = positions[0];10851086// Executing the edit might free the completion, so we have to hold a reference on it.1087completion.addRef();1088try {1089this._isAcceptingPartially = true;1090try {1091editor.pushUndoStop();1092const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos);1093const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal;1094const primaryEdit = new TextReplacement(replaceRange, newText);1095const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)].filter(isDefined);1096const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p));10971098editor.edit(TextEdit.fromParallelReplacementsUnsorted(edits), this._getMetadata(completion, type));1099editor.setSelections(selections, 'inlineCompletionPartialAccept');1100editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Smooth);1101} finally {1102this._isAcceptingPartially = false;1103}11041105const acceptedRange = Range.fromPositions(completion.editRange.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos));1106// This assumes that the inline completion and the model use the same EOL style.1107const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF);1108const acceptedLength = text.length;1109completion.reportPartialAccept(1110acceptedLength,1111{ kind, acceptedLength: acceptedLength },1112{ characters: acceptUntilIndexExclusive, ratio: acceptUntilIndexExclusive / ghostTextVal.length, count: 1 }1113);11141115} finally {1116completion.removeRef();1117}1118}11191120public handleSuggestAccepted(item: SuggestItemInfo) {1121const itemEdit = singleTextRemoveCommonPrefix(item.getSingleTextEdit(), this.textModel);1122const augmentedCompletion = this._computeAugmentation(itemEdit, undefined);1123if (!augmentedCompletion) { return; }11241125// This assumes that the inline completion and the model use the same EOL style.1126const alreadyAcceptedLength = this.textModel.getValueInRange(augmentedCompletion.completion.editRange, EndOfLinePreference.LF).length;1127const acceptedLength = alreadyAcceptedLength + itemEdit.text.length;11281129augmentedCompletion.completion.reportPartialAccept(itemEdit.text.length, {1130kind: PartialAcceptTriggerKind.Suggest,1131acceptedLength,1132}, {1133characters: itemEdit.text.length,1134count: 1,1135ratio: 11136});1137}11381139public extractReproSample(): Repro {1140const value = this.textModel.getValue();1141const item = this.state.get()?.inlineSuggestion;1142return {1143documentValue: value,1144inlineCompletion: item?.getSourceCompletion(),1145};1146}11471148private readonly _jumpedToId = observableValue<undefined | string>(this, undefined);1149private readonly _inAcceptFlow = observableValue(this, false);1150public readonly inAcceptFlow: IObservable<boolean> = this._inAcceptFlow;11511152public jump(): void {1153const s = this.inlineEditState.get();1154if (!s) { return; }11551156const suggestion = s.inlineSuggestion;11571158if (!suggestion.originalTextRef.targets(this.textModel)) {1159this.accept(this._editor);1160return;1161}116211631164suggestion.addRef();1165try {1166transaction(tx => {1167if (suggestion.action?.kind === 'jumpTo') {1168this.stop(undefined, tx);1169suggestion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction: false });1170}11711172this._jumpedToId.set(s.inlineSuggestion.semanticId, tx);1173this.dontRefetchSignal.trigger(tx);1174const targetRange = s.inlineSuggestion.targetRange;1175const targetPosition = targetRange.getStartPosition();1176this._editor.setPosition(targetPosition, 'inlineCompletions.jump');11771178// TODO: consider using view information to reveal it1179const isSingleLineChange = targetRange.isSingleLine() && (s.inlineSuggestion.hint || (s.inlineSuggestion.action?.kind === 'edit' && !s.inlineSuggestion.action.textReplacement.text.includes('\n')));1180if (isSingleLineChange || s.inlineSuggestion.action?.kind === 'jumpTo') {1181this._editor.revealPosition(targetPosition, ScrollType.Smooth);1182} else {1183const revealRange = new Range(targetRange.startLineNumber - 1, 1, targetRange.endLineNumber + 1, 1);1184this._editor.revealRange(revealRange, ScrollType.Smooth);1185}11861187s.inlineSuggestion.identity.setJumpTo(tx);11881189this._editor.focus();1190});1191} finally {1192suggestion.removeRef();1193}1194}11951196public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, timeWhenShown: number): Promise<void> {1197await inlineCompletion.reportInlineEditShown(this._commandService, viewKind, viewData, this.textModel, timeWhenShown);1198}11991200/**1201* Transplants an inline completion from another model to this one.1202* Used for cross-file inline edits.1203*/1204public transplantCompletion(item: InlineSuggestionItem): void {1205item.addRef();1206transaction(tx => {1207this._source.seedWithCompletion(item, tx);1208this._isActive.set(true, tx);1209this._inAcceptFlow.set(true, tx);1210this.dontRefetchSignal.trigger(tx);1211});1212}1213}12141215interface Repro {1216documentValue: string;1217inlineCompletion: InlineCompletion | undefined;1218}12191220export enum VersionIdChangeReason {1221Undo,1222Redo,1223AcceptWord,1224Other,1225}12261227export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryTextRepl: TextReplacement): (TextReplacement | undefined)[] {1228if (positions.length === 1) {1229// No secondary cursor positions1230return [];1231}1232const text = new TextModelText(textModel);1233const textTransformer = text.getTransformer();1234const primaryOffset = textTransformer.getOffset(positions[0]);1235const secondaryOffsets = positions.slice(1).map(pos => textTransformer.getOffset(pos));12361237primaryTextRepl = primaryTextRepl.removeCommonPrefixAndSuffix(text);1238const primaryStringRepl = textTransformer.getStringReplacement(primaryTextRepl);12391240const deltaFromOffsetToRangeStart = primaryStringRepl.replaceRange.start - primaryOffset;1241const primaryContextRange = primaryStringRepl.replaceRange.join(OffsetRange.emptyAt(primaryOffset));1242const primaryContextValue = text.getValueOfOffsetRange(primaryContextRange);12431244const replacements = secondaryOffsets.map(secondaryOffset => {1245const newRangeStart = secondaryOffset + deltaFromOffsetToRangeStart;1246const newRangeEnd = newRangeStart + primaryStringRepl.replaceRange.length;1247const range = new OffsetRange(newRangeStart, newRangeEnd);12481249const contextRange = range.join(OffsetRange.emptyAt(secondaryOffset));1250const contextValue = text.getValueOfOffsetRange(contextRange);1251if (contextValue !== primaryContextValue) {1252return undefined;1253}12541255const stringRepl = new StringReplacement(range, primaryStringRepl.newText);1256const repl = textTransformer.getTextReplacement(stringRepl);1257return repl;1258}).filter(isDefined);12591260return replacements;1261}12621263class FadeoutDecoration extends Disposable {1264constructor(1265editor: ICodeEditor,1266ranges: Range[],1267onDispose?: () => void,1268) {1269super();12701271if (onDispose) {1272this._register({ dispose: () => onDispose() });1273}12741275this._register(observableCodeEditor(editor).setDecorations(constObservable(ranges.map<IModelDeltaDecoration>(range => ({1276range: range,1277options: {1278description: 'animation',1279className: 'edits-fadeout-decoration',1280zIndex: 1,1281}1282})))));12831284const animation = new AnimatedValue(1, 0, 1000, easeOutCubic);1285const val = new ObservableAnimatedValue(animation);12861287this._register(autorun(reader => {1288const opacity = val.getValue(reader);1289editor.getContainerDomNode().style.setProperty('--animation-opacity', opacity.toString());1290if (animation.isFinished()) {1291this.dispose();1292}1293}));1294}1295}12961297export function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem, reader: IReader | undefined = undefined): boolean {1298const targetRange = suggestion.targetRange;12991300// TODO make getVisibleRanges reactive!1301observableCodeEditor(editor).scrollTop.read(reader);1302const visibleRanges = editor.getVisibleRanges();13031304if (visibleRanges.length < 1) {1305return false;1306}13071308const viewportRange = new Range(1309visibleRanges[0].startLineNumber,1310visibleRanges[0].startColumn,1311visibleRanges[visibleRanges.length - 1].endLineNumber,1312visibleRanges[visibleRanges.length - 1].endColumn1313);1314return viewportRange.containsRange(targetRange);1315}13161317function skuFromAccount(account: IDefaultAccount | null): InlineSuggestSku | undefined {1318if (account?.entitlementsData?.access_type_sku && account?.entitlementsData?.copilot_plan) {1319return { type: account.entitlementsData.access_type_sku, plan: account.entitlementsData.copilot_plan };1320}1321return undefined;1322}13231324class DisposableCallback<T> {1325private _cb: ((e: T) => void) | undefined;13261327constructor(cb: (e: T) => void) {1328this._cb = cb;1329}13301331dispose(): void {1332this._cb = undefined;1333}13341335readonly handler = (val: T) => {1336return this._cb?.(val);1337};1338}13391340function createDisposableCb<T>(cb: (e: T) => void, store: DisposableStore): (e: T) => void {1341const dcb = new DisposableCallback(cb);1342store.add(dcb);1343return dcb.handler;1344}134513461347