Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.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 { 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 { 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';5556export class InlineCompletionsModel extends Disposable {57private readonly _source;58private readonly _isActive = observableValue<boolean>(this, false);59private readonly _onlyRequestInlineEditsSignal = observableSignal(this);60private readonly _forceUpdateExplicitlySignal = observableSignal(this);61private readonly _noDelaySignal = observableSignal(this);6263private readonly _fetchSpecificProviderSignal = observableSignal<InlineCompletionsProvider | undefined>(this);6465// We use a semantic id to keep the same inline completion selected even if the provider reorders the completions.66private readonly _selectedInlineCompletionId = observableValue<string | undefined>(this, undefined);67public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1));68public readonly allPositions = derived(this, reader => this._positions.read(reader));6970private readonly sku = observableValue<InlineSuggestSku | undefined>(this, undefined);7172private _isAcceptingPartially = false;73private readonly _appearedInsideViewport = derived<boolean>(this, reader => {74const state = this.state.read(reader);75if (!state || !state.inlineSuggestion) {76return false;77}7879return isSuggestionInViewport(this._editor, state.inlineSuggestion);80});81public get isAcceptingPartially() { return this._isAcceptingPartially; }8283private readonly _onDidAccept = new Emitter<void>();84public readonly onDidAccept = this._onDidAccept.event;8586private readonly _editorObs;8788private readonly _typing: TypingInterval;8990private readonly _suggestPreviewEnabled;91private readonly _suggestPreviewMode;92private readonly _inlineSuggestMode;93private readonly _suppressedInlineCompletionGroupIds;94private readonly _inlineEditsEnabled;95private readonly _inlineEditsShowCollapsedEnabled;96private readonly _triggerCommandOnProviderChange;97private readonly _minShowDelay;98private readonly _showOnSuggestConflict;99private readonly _suppressInSnippetMode;100private readonly _isInSnippetMode;101102get editor() {103return this._editor;104}105106constructor(107public readonly textModel: ITextModel,108private readonly _selectedSuggestItem: IObservable<SuggestItemInfo | undefined>,109public readonly _textModelVersionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>,110private readonly _positions: IObservable<readonly Position[]>,111private readonly _debounceValue: IFeatureDebounceInformation,112private readonly _enabled: IObservable<boolean>,113private readonly _editor: ICodeEditor,114@IInstantiationService private readonly _instantiationService: IInstantiationService,115@ICommandService private readonly _commandService: ICommandService,116@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,117@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,118@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,119@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,120@IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService,121@IDefaultAccountService defaultAccountService: IDefaultAccountService,122) {123super();124this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue, this.primaryPosition));125this.lastTriggerKind = this._source.inlineCompletions.map(this, v => v?.request?.context.triggerKind);126127this._editorObs = observableCodeEditor(this._editor);128129const suggest = this._editorObs.getOption(EditorOption.suggest);130this._suggestPreviewEnabled = suggest.map(v => v.preview);131this._suggestPreviewMode = suggest.map(v => v.previewMode);132133const inlineSuggest = this._editorObs.getOption(EditorOption.inlineSuggest);134this._inlineSuggestMode = inlineSuggest.map(v => v.mode);135this._suppressedInlineCompletionGroupIds = inlineSuggest.map(v => new Set(v.experimental.suppressInlineSuggestions.split(',')));136this._inlineEditsEnabled = inlineSuggest.map(v => !!v.edits.enabled);137this._inlineEditsShowCollapsedEnabled = inlineSuggest.map(s => s.edits.showCollapsed);138this._triggerCommandOnProviderChange = inlineSuggest.map(s => s.triggerCommandOnProviderChange);139this._minShowDelay = inlineSuggest.map(s => s.minShowDelay);140this._showOnSuggestConflict = inlineSuggest.map(s => s.experimental.showOnSuggestConflict);141this._suppressInSnippetMode = inlineSuggest.map(s => s.suppressInSnippetMode);142143const snippetController = SnippetController2.get(this._editor);144this._isInSnippetMode = snippetController?.isInSnippetObservable ?? constObservable(false);145146defaultAccountService.getDefaultAccount().then(createDisposableCb(account => this.sku.set(skuFromAccount(account), undefined), this._store));147this._register(defaultAccountService.onDidChangeDefaultAccount(account => this.sku.set(skuFromAccount(account), undefined)));148149this._typing = this._register(new TypingInterval(this.textModel));150151this._register(this._inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => {152if (isSnoozing) {153this.stop();154}155}));156157{ // Determine editor type158const isNotebook = this.textModel.uri.scheme === Schemas.vscodeNotebookCell;159const [diffEditor] = this._codeEditorService.listDiffEditors()160.filter(d =>161d.getOriginalEditor().getId() === this._editor.getId() ||162d.getModifiedEditor().getId() === this._editor.getId());163164this.isInDiffEditor = !!diffEditor;165this.editorType = isNotebook ? InlineCompletionEditorType.Notebook166: this.isInDiffEditor ? InlineCompletionEditorType.DiffEditor167: InlineCompletionEditorType.TextEditor;168}169170this._register(recomputeInitiallyAndOnChange(this.state, (s) => {171if (s && s.inlineSuggestion) {172this._inlineCompletionsService.reportNewCompletion(s.inlineSuggestion.requestUuid);173}174}));175176this._register(recomputeInitiallyAndOnChange(this._fetchInlineCompletionsPromise));177178this._register(autorun(reader => {179this._editorObs.versionId.read(reader);180this._inAcceptFlow.set(false, undefined);181}));182183this._register(autorun(reader => {184const jumpToReset = this.state.map((s, reader) => !s || s.kind === 'inlineEdit' && !s.cursorAtInlineEdit.read(reader)).read(reader);185if (jumpToReset) {186this._jumpedToId.set(undefined, undefined);187}188}));189190this._register(autorun(reader => {191const inlineSuggestion = this.state.map(s => s?.inlineSuggestion).read(reader);192if (inlineSuggestion) {193inlineSuggestion.addPerformanceMarker('activeSuggestion');194}195}));196197const inlineEditSemanticId = this.inlineEditState.map(s => s?.inlineSuggestion.semanticId);198199this._register(autorun(reader => {200const id = inlineEditSemanticId.read(reader);201if (id) {202this._editor.pushUndoStop();203this._lastShownInlineCompletionInfo = {204alternateTextModelVersionId: this.textModel.getAlternativeVersionId(),205inlineCompletion: this.state.get()!.inlineSuggestion!,206};207}208}));209210// TODO: should use getAvailableProviders and update on _suppressedInlineCompletionGroupIds change211const inlineCompletionProviders = observableFromEvent(this._languageFeaturesService.inlineCompletionsProvider.onDidChange, () => this._languageFeaturesService.inlineCompletionsProvider.all(textModel));212mapObservableArrayCached(this, inlineCompletionProviders, (provider, store) => {213if (!provider.onDidChangeInlineCompletions) {214return;215}216217store.add(provider.onDidChangeInlineCompletions(() => {218if (!this._enabled.get()) {219return;220}221222// Only update the active editor223const activeEditor = this._codeEditorService.getFocusedCodeEditor() || this._codeEditorService.getActiveCodeEditor();224if (activeEditor !== this._editor) {225return;226}227228if (this._triggerCommandOnProviderChange.get()) {229// TODO@hediet remove this and always do the else branch.230this.trigger(undefined, { onlyFetchInlineEdits: true });231return;232}233234235// If there is an active suggestion from a different provider, we ignore the update236const activeState = this.state.get();237if (activeState && (activeState.inlineSuggestion || activeState.edits) && activeState.inlineSuggestion?.source.provider !== provider) {238return;239}240241transaction(tx => {242this._fetchSpecificProviderSignal.trigger(tx, provider);243this.trigger(tx);244});245246}));247}).recomputeInitiallyAndOnChange(this._store);248249this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store);250}251252private _lastShownInlineCompletionInfo: { alternateTextModelVersionId: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined;253private _lastAcceptedInlineCompletionInfo: { textModelVersionIdAfter: number; /* already freed! */ inlineCompletion: InlineSuggestionItem } | undefined = undefined;254private readonly _didUndoInlineEdits = derivedHandleChanges({255owner: this,256changeTracker: {257createChangeSummary: () => ({ didUndo: false }),258handleChange: (ctx, changeSummary) => {259changeSummary.didUndo = ctx.didChange(this._textModelVersionId) && !!ctx.change?.isUndoing;260return true;261}262}263}, (reader, changeSummary) => {264const versionId = this._textModelVersionId.read(reader);265if (versionId !== null266&& this._lastAcceptedInlineCompletionInfo267&& this._lastAcceptedInlineCompletionInfo.textModelVersionIdAfter === versionId - 1268&& this._lastAcceptedInlineCompletionInfo.inlineCompletion.isInlineEdit269&& changeSummary.didUndo270) {271this._lastAcceptedInlineCompletionInfo = undefined;272return true;273}274return false;275});276277public debugGetSelectedSuggestItem(): IObservable<SuggestItemInfo | undefined> {278return this._selectedSuggestItem;279}280281public getIndentationInfo(reader: IReader) {282let startsWithIndentation = false;283let startsWithIndentationLessThanTabSize = true;284const ghostText = this?.primaryGhostText.read(reader);285if (!!this?._selectedSuggestItem && ghostText && ghostText.parts.length > 0) {286const { column, lines } = ghostText.parts[0];287288const firstLine = lines[0].line;289290const indentationEndColumn = this.textModel.getLineIndentColumn(ghostText.lineNumber);291const inIndentation = column <= indentationEndColumn;292293if (inIndentation) {294let firstNonWsIdx = firstNonWhitespaceIndex(firstLine);295if (firstNonWsIdx === -1) {296firstNonWsIdx = firstLine.length - 1;297}298startsWithIndentation = firstNonWsIdx > 0;299300const tabSize = this.textModel.getOptions().tabSize;301const visibleColumnIndentation = CursorColumns.visibleColumnFromColumn(firstLine, firstNonWsIdx + 1, tabSize);302startsWithIndentationLessThanTabSize = visibleColumnIndentation < tabSize;303}304}305return {306startsWithIndentation,307startsWithIndentationLessThanTabSize,308};309}310311private readonly _preserveCurrentCompletionReasons = new Set([312VersionIdChangeReason.Redo,313VersionIdChangeReason.Undo,314VersionIdChangeReason.AcceptWord,315]);316317private _getReason(e: IModelContentChangedEvent | undefined): VersionIdChangeReason {318if (e?.isUndoing) { return VersionIdChangeReason.Undo; }319if (e?.isRedoing) { return VersionIdChangeReason.Redo; }320if (this.isAcceptingPartially) { return VersionIdChangeReason.AcceptWord; }321return VersionIdChangeReason.Other;322}323324public readonly dontRefetchSignal = observableSignal(this);325326private readonly _fetchInlineCompletionsPromise = derivedHandleChanges({327owner: this,328changeTracker: {329createChangeSummary: () => ({330dontRefetch: false,331preserveCurrentCompletion: false,332inlineCompletionTriggerKind: InlineCompletionTriggerKind.Automatic,333onlyRequestInlineEdits: false,334shouldDebounce: true,335provider: undefined as InlineCompletionsProvider | undefined,336textChange: false,337changeReason: '',338}),339handleChange: (ctx, changeSummary) => {340/** @description fetch inline completions */341if (ctx.didChange(this._textModelVersionId)) {342if (this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) {343changeSummary.preserveCurrentCompletion = true;344}345const detailedReasons = ctx.change?.detailedReasons ?? [];346changeSummary.changeReason = detailedReasons.length > 0 ? detailedReasons[0].getType() : '';347changeSummary.textChange = true;348} else if (ctx.didChange(this._forceUpdateExplicitlySignal)) {349changeSummary.preserveCurrentCompletion = true;350changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit;351} else if (ctx.didChange(this.dontRefetchSignal)) {352changeSummary.dontRefetch = true;353} else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) {354changeSummary.onlyRequestInlineEdits = true;355} else if (ctx.didChange(this._fetchSpecificProviderSignal)) {356changeSummary.provider = ctx.change;357}358return true;359},360},361}, (reader, changeSummary) => {362this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation363this._noDelaySignal.read(reader);364this.dontRefetchSignal.read(reader);365this._onlyRequestInlineEditsSignal.read(reader);366this._forceUpdateExplicitlySignal.read(reader);367this._fetchSpecificProviderSignal.read(reader);368const shouldUpdate = ((this._enabled.read(reader) && this._selectedSuggestItem.read(reader)) || this._isActive.read(reader))369&& (!this._inlineCompletionsService.isSnoozing() || changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit);370if (!shouldUpdate) {371this._source.cancelUpdate();372return undefined;373}374375this._textModelVersionId.read(reader); // Refetch on text change376377const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(undefined);378let suggestItem = this._selectedSuggestItem.read(reader);379if (this._shouldShowOnSuggestConflict.read(undefined)) {380suggestItem = undefined;381}382if (suggestWidgetInlineCompletions && !suggestItem) {383this._source.seedInlineCompletionsWithSuggestWidget();384}385386if (changeSummary.dontRefetch) {387return Promise.resolve(true);388}389390if (this._didUndoInlineEdits.read(reader) && changeSummary.inlineCompletionTriggerKind !== InlineCompletionTriggerKind.Explicit) {391transaction(tx => {392this._source.clear(tx);393});394return undefined;395}396397let reason: string = '';398if (changeSummary.provider) {399reason += 'providerOnDidChange';400} else if (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit) {401reason += 'explicit';402}403if (changeSummary.changeReason) {404reason += reason.length > 0 ? `:${changeSummary.changeReason}` : changeSummary.changeReason;405}406407const typingInterval = this._typing.getTypingInterval();408const requestInfo: InlineSuggestRequestInfo = {409editorType: this.editorType,410startTime: Date.now(),411languageId: this.textModel.getLanguageId(),412reason,413typingInterval: typingInterval.averageInterval,414typingIntervalCharacterCount: typingInterval.characterCount,415availableProviders: [],416sku: this.sku.read(undefined),417};418419let context: InlineCompletionContextWithoutUuid = {420triggerKind: changeSummary.inlineCompletionTriggerKind,421selectedSuggestionInfo: suggestItem?.toSelectedSuggestionInfo(),422includeInlineCompletions: !changeSummary.onlyRequestInlineEdits,423includeInlineEdits: this._inlineEditsEnabled.read(reader),424requestIssuedDateTime: requestInfo.startTime,425earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.read(undefined) ? 0 : this._minShowDelay.read(undefined)),426};427428if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) {429if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) {430// When undoing back to a version where an inline edit/completion was shown,431// we want to show an inline edit (or completion) again if it was originally an inline edit (or completion).432context = {433...context,434includeInlineCompletions: !this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit,435includeInlineEdits: this._lastShownInlineCompletionInfo.inlineCompletion.isInlineEdit,436};437}438}439440const itemToPreserveCandidate = this.selectedInlineCompletion.read(undefined) ?? this._inlineCompletionItems.read(undefined)?.inlineEdit;441const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable442? itemToPreserveCandidate : undefined;443const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.read(undefined)?.inlineEdit?.semanticId);444445const providers = changeSummary.provider446? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId?.toString() }447: { providers: this._languageFeaturesService.inlineCompletionsProvider.all(this.textModel), label: undefined }; // TODO: should use inlineCompletionProviders448const availableProviders = this.getAvailableProviders(providers.providers);449requestInfo.availableProviders = availableProviders.map(p => p.providerId).filter(isDefined);450451return this._source.fetch(availableProviders, providers.label, context, itemToPreserve?.identity, changeSummary.shouldDebounce, userJumpedToActiveCompletion, requestInfo);452});453454// TODO: This is not an ideal implementation of excludesGroupIds, however as this is currently still behind proposed API455// and due to the time constraints, we are using a simplified approach456private getAvailableProviders(providers: InlineCompletionsProvider[]): InlineCompletionsProvider[] {457const suppressedProviderGroupIds = this._suppressedInlineCompletionGroupIds.get();458const unsuppressedProviders = providers.filter(provider => !(provider.groupId && suppressedProviderGroupIds.has(provider.groupId)));459460const excludedGroupIds = new Set<string>();461for (const provider of unsuppressedProviders) {462provider.excludesGroupIds?.forEach(p => excludedGroupIds.add(p));463}464465const availableProviders: InlineCompletionsProvider[] = [];466for (const provider of unsuppressedProviders) {467if (provider.groupId && excludedGroupIds.has(provider.groupId)) {468continue;469}470availableProviders.push(provider);471}472473return availableProviders;474}475476public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean } = {}): Promise<void> {477subtransaction(tx, tx => {478if (options.onlyFetchInlineEdits) {479this._onlyRequestInlineEditsSignal.trigger(tx);480}481if (options.noDelay) {482this._noDelaySignal.trigger(tx);483}484this._isActive.set(true, tx);485486if (options.explicit) {487this._inAcceptFlow.set(true, tx);488this._forceUpdateExplicitlySignal.trigger(tx);489}490if (options.provider) {491this._fetchSpecificProviderSignal.trigger(tx, options.provider);492}493});494await this._fetchInlineCompletionsPromise.get();495}496497public async triggerExplicitly(tx?: ITransaction, onlyFetchInlineEdits: boolean = false): Promise<void> {498return this.trigger(tx, { onlyFetchInlineEdits, explicit: true });499}500501public stop(stopReason: 'explicitCancel' | 'automatic' = 'automatic', tx?: ITransaction): void {502subtransaction(tx, tx => {503if (stopReason === 'explicitCancel') {504const inlineCompletion = this.state.get()?.inlineSuggestion;505if (inlineCompletion) {506inlineCompletion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Rejected });507}508}509510this._isActive.set(false, tx);511this._source.clear(tx);512});513}514515private readonly _inlineCompletionItems = derivedOpts({ owner: this }, reader => {516const c = this._source.inlineCompletions.read(reader);517if (!c) { return undefined; }518const cursorPosition = this.primaryPosition.read(reader);519let inlineEdit: InlineEditItem | undefined = undefined;520const visibleCompletions: InlineCompletionItem[] = [];521for (const completion of c.inlineCompletions) {522if (!completion.isInlineEdit) {523if (completion.isVisible(this.textModel, cursorPosition)) {524visibleCompletions.push(completion);525}526} else {527inlineEdit = completion;528}529}530531if (visibleCompletions.length !== 0) {532// Don't show the inline edit if there is a visible completion533inlineEdit = undefined;534}535536return {537inlineCompletions: visibleCompletions,538inlineEdit,539};540});541542private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: arrayEqualsC() }, reader => {543const c = this._inlineCompletionItems.read(reader);544return c?.inlineCompletions ?? [];545});546547public readonly selectedInlineCompletionIndex = derived<number>(this, (reader) => {548const selectedInlineCompletionId = this._selectedInlineCompletionId.read(reader);549const filteredCompletions = this._filteredInlineCompletionItems.read(reader);550const idx = this._selectedInlineCompletionId === undefined ? -1551: filteredCompletions.findIndex(v => v.semanticId === selectedInlineCompletionId);552if (idx === -1) {553// Reset the selection so that the selection does not jump back when it appears again554this._selectedInlineCompletionId.set(undefined, undefined);555return 0;556}557return idx;558});559560public readonly selectedInlineCompletion = derived<InlineCompletionItem | undefined>(this, (reader) => {561const filteredCompletions = this._filteredInlineCompletionItems.read(reader);562const idx = this.selectedInlineCompletionIndex.read(reader);563return filteredCompletions[idx];564});565566public readonly activeCommands = derivedOpts<InlineCompletionCommand[]>({ owner: this, equalsFn: arrayEqualsC() },567r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? []568);569570public readonly lastTriggerKind: IObservable<InlineCompletionTriggerKind | undefined>;571572public readonly inlineCompletionsCount = derived<number | undefined>(this, reader => {573if (this.lastTriggerKind.read(reader) === InlineCompletionTriggerKind.Explicit) {574return this._filteredInlineCompletionItems.read(reader).length;575} else {576return undefined;577}578});579580private readonly _hasVisiblePeekWidgets = derived(this, reader => this._editorObs.openedPeekWidgets.read(reader) > 0);581582private readonly _shouldShowOnSuggestConflict = derived(this, reader => {583const showOnSuggestConflict = this._showOnSuggestConflict.read(reader);584if (showOnSuggestConflict !== 'never') {585const hasInlineCompletion = !!this.selectedInlineCompletion.read(reader);586if (hasInlineCompletion) {587const item = this._selectedSuggestItem.read(reader);588if (!item) {589return false;590}591if (showOnSuggestConflict === 'whenSuggestListIsIncomplete') {592return item.listIncomplete;593}594return true;595}596}597return false;598});599600public readonly state = derivedOpts<{601kind: 'ghostText';602edits: readonly TextReplacement[];603primaryGhostText: GhostTextOrReplacement;604ghostTexts: readonly GhostTextOrReplacement[];605suggestItem: SuggestItemInfo | undefined;606inlineSuggestion: InlineCompletionItem | undefined;607} | {608kind: 'inlineEdit';609edits: readonly TextReplacement[];610inlineSuggestion: InlineEditItem;611cursorAtInlineEdit: IObservable<boolean>;612nextEditUri: URI | undefined;613} | undefined>({614owner: this,615equalsFn: (a, b) => {616if (!a || !b) { return a === b; }617618if (a.kind === 'ghostText' && b.kind === 'ghostText') {619return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts)620&& a.inlineSuggestion === b.inlineSuggestion621&& a.suggestItem === b.suggestItem;622} else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') {623return a.inlineSuggestion === b.inlineSuggestion;624}625return false;626}627}, (reader) => {628const model = this.textModel;629630if (this._suppressInSnippetMode.read(reader) && this._isInSnippetMode.read(reader)) {631return undefined;632}633634const item = this._inlineCompletionItems.read(reader);635const inlineEditResult = item?.inlineEdit;636if (inlineEditResult) {637if (this._hasVisiblePeekWidgets.read(reader)) {638return undefined;639}640const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber));641const stringEdit = inlineEditResult.action?.kind === 'edit' ? inlineEditResult.action.stringEdit : undefined;642const replacements = stringEdit ? TextEdit.fromStringEdit(stringEdit, new TextModelText(this.textModel)).replacements : [];643644const nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') &&645// eslint-disable-next-line local/code-no-any-casts646item.inlineEdit?.command.arguments?.length ? URI.from(<any>item.inlineEdit?.command.arguments[0]) : undefined;647return { kind: 'inlineEdit', inlineSuggestion: inlineEditResult, edits: replacements, cursorAtInlineEdit, nextEditUri };648}649650const suggestItem = this._selectedSuggestItem.read(reader);651if (!this._shouldShowOnSuggestConflict.read(reader) && suggestItem) {652const suggestCompletionEdit = singleTextRemoveCommonPrefix(suggestItem.getSingleTextEdit(), model);653const augmentation = this._computeAugmentation(suggestCompletionEdit, reader);654655const isSuggestionPreviewEnabled = this._suggestPreviewEnabled.read(reader);656if (!isSuggestionPreviewEnabled && !augmentation) { return undefined; }657658const fullEdit = augmentation?.edit ?? suggestCompletionEdit;659const fullEditPreviewLength = augmentation ? augmentation.edit.text.length - suggestCompletionEdit.text.length : 0;660661const mode = this._suggestPreviewMode.read(reader);662const positions = this._positions.read(reader);663const allPotentialEdits = [fullEdit, ...getSecondaryEdits(this.textModel, positions, fullEdit)];664const validEditsAndGhostTexts = allPotentialEdits665.map((edit, idx) => ({ edit, ghostText: edit ? computeGhostText(edit, model, mode, positions[idx], fullEditPreviewLength) : undefined }))666.filter(({ edit, ghostText }) => edit !== undefined && ghostText !== undefined);667const edits = validEditsAndGhostTexts.map(({ edit }) => edit!);668const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!);669const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []);670return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineSuggestion: augmentation?.completion, suggestItem };671} else {672if (!this._isActive.read(reader)) { return undefined; }673const inlineSuggestion = this.selectedInlineCompletion.read(reader);674if (!inlineSuggestion) { return undefined; }675676const replacement = inlineSuggestion.getSingleTextEdit();677const mode = this._inlineSuggestMode.read(reader);678const positions = this._positions.read(reader);679const allPotentialEdits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)];680const validEditsAndGhostTexts = allPotentialEdits681.map((edit, idx) => ({ edit, ghostText: edit ? computeGhostText(edit, model, mode, positions[idx], 0) : undefined }))682.filter(({ edit, ghostText }) => edit !== undefined && ghostText !== undefined);683const edits = validEditsAndGhostTexts.map(({ edit }) => edit!);684const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!);685if (!ghostTexts[0]) { return undefined; }686return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineSuggestion, suggestItem: undefined };687}688});689690public readonly status = derived(this, reader => {691if (this._source.loading.read(reader)) { return 'loading'; }692const s = this.state.read(reader);693if (s?.kind === 'ghostText') { return 'ghostText'; }694if (s?.kind === 'inlineEdit') { return 'inlineEdit'; }695return 'noSuggestion';696});697698public readonly inlineCompletionState = derived(this, reader => {699const s = this.state.read(reader);700if (!s || s.kind !== 'ghostText') {701return undefined;702}703if (this._editorObs.inComposition.read(reader)) {704return undefined;705}706return s;707});708709public readonly inlineEditState = derived(this, reader => {710const s = this.state.read(reader);711if (!s || s.kind !== 'inlineEdit') {712return undefined;713}714return s;715});716717public readonly inlineEditAvailable = derived(this, reader => {718const s = this.inlineEditState.read(reader);719return !!s;720});721722private _computeAugmentation(suggestCompletion: TextReplacement, reader: IReader | undefined) {723const model = this.textModel;724const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(reader);725const candidateInlineCompletions = suggestWidgetInlineCompletions726? suggestWidgetInlineCompletions.inlineCompletions.filter(c => !c.isInlineEdit)727: [this.selectedInlineCompletion.read(reader)].filter(isDefined);728729const augmentedCompletion = mapFindFirst(candidateInlineCompletions, completion => {730let r = completion.getSingleTextEdit();731r = singleTextRemoveCommonPrefix(732r,733model,734Range.fromPositions(r.range.getStartPosition(), suggestCompletion.range.getEndPosition())735);736return singleTextEditAugments(r, suggestCompletion) ? { completion, edit: r } : undefined;737});738739return augmentedCompletion;740}741742public readonly warning = derived(this, reader => {743return this.inlineCompletionState.read(reader)?.inlineSuggestion?.warning;744});745746public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => {747const v = this.inlineCompletionState.read(reader);748if (!v) {749return undefined;750}751return v.ghostTexts;752});753754public readonly primaryGhostText = derivedOpts({ owner: this, equalsFn: ghostTextOrReplacementEquals }, reader => {755const v = this.inlineCompletionState.read(reader);756if (!v) {757return undefined;758}759return v?.primaryGhostText;760});761762public readonly showCollapsed = derived<boolean>(this, reader => {763const state = this.state.read(reader);764if (!state || state.kind !== 'inlineEdit') {765return false;766}767768if (state.inlineSuggestion.hint || state.inlineSuggestion.action?.kind === 'jumpTo') {769return false;770}771772const isCurrentModelVersion = state.inlineSuggestion.updatedEditModelVersion === this._textModelVersionId.read(reader);773return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion)774&& this._jumpedToId.read(reader) !== state.inlineSuggestion.semanticId775&& !this._inAcceptFlow.read(reader);776});777778private readonly _tabShouldIndent = derived(this, reader => {779if (this._inAcceptFlow.read(reader)) {780return false;781}782783function isMultiLine(range: Range): boolean {784return range.startLineNumber !== range.endLineNumber;785}786787function getNonIndentationRange(model: ITextModel, lineNumber: number): Range {788const columnStart = model.getLineIndentColumn(lineNumber);789const lastNonWsColumn = model.getLineLastNonWhitespaceColumn(lineNumber);790const columnEnd = Math.max(lastNonWsColumn, columnStart);791return new Range(lineNumber, columnStart, lineNumber, columnEnd);792}793794const selections = this._editorObs.selections.read(reader);795return selections?.some(s => {796if (s.isEmpty()) {797return this.textModel.getLineLength(s.startLineNumber) === 0;798} else {799return isMultiLine(s) || s.containsRange(getNonIndentationRange(this.textModel, s.startLineNumber));800}801});802});803804public readonly tabShouldJumpToInlineEdit = derived(this, reader => {805if (this._tabShouldIndent.read(reader)) {806return false;807}808809const s = this.inlineEditState.read(reader);810if (!s) {811return false;812}813814815if (s.inlineSuggestion.action?.kind === 'jumpTo') {816return true;817}818819if (this.showCollapsed.read(reader)) {820return true;821}822823if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) {824return false;825}826827return !s.cursorAtInlineEdit.read(reader);828});829830public readonly tabShouldAcceptInlineEdit = derived(this, reader => {831const s = this.inlineEditState.read(reader);832if (!s) {833return false;834}835if (s.inlineSuggestion.action?.kind === 'jumpTo') {836return false;837}838if (this.showCollapsed.read(reader)) {839return false;840}841if (this._tabShouldIndent.read(reader)) {842return false;843}844if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) {845return true;846}847if (s.inlineSuggestion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) {848return true;849}850if (this._jumpedToId.read(reader) === s.inlineSuggestion.semanticId) {851return true;852}853854return s.cursorAtInlineEdit.read(reader);855});856857public readonly isInDiffEditor;858859public readonly editorType: InlineCompletionEditorType;860861private async _deltaSelectedInlineCompletionIndex(delta: 1 | -1): Promise<void> {862await this.triggerExplicitly();863864const completions = this._filteredInlineCompletionItems.get() || [];865if (completions.length > 0) {866const newIdx = (this.selectedInlineCompletionIndex.get() + delta + completions.length) % completions.length;867this._selectedInlineCompletionId.set(completions[newIdx].semanticId, undefined);868} else {869this._selectedInlineCompletionId.set(undefined, undefined);870}871}872873public async next(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(1); }874875public async previous(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(-1); }876877private _getMetadata(completion: InlineSuggestionItem, languageId: string, type: 'word' | 'line' | undefined = undefined): TextModelEditSource {878if (type) {879return EditSources.inlineCompletionPartialAccept({880nes: completion.isInlineEdit,881requestUuid: completion.requestUuid,882providerId: completion.source.provider.providerId,883languageId,884type,885correlationId: completion.getSourceCompletion().correlationId,886});887} else {888return EditSources.inlineCompletionAccept({889nes: completion.isInlineEdit,890requestUuid: completion.requestUuid,891correlationId: completion.getSourceCompletion().correlationId,892providerId: completion.source.provider.providerId,893languageId894});895}896}897898public async accept(editor: ICodeEditor = this._editor, alternativeAction: boolean = false): Promise<void> {899if (editor.getModel() !== this.textModel) {900throw new BugIndicatingError();901}902903let completion: InlineSuggestionItem;904let isNextEditUri = false;905const state = this.state.get();906if (state?.kind === 'ghostText') {907if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) {908return;909}910completion = state.inlineSuggestion;911} else if (state?.kind === 'inlineEdit') {912completion = state.inlineSuggestion;913isNextEditUri = !!state.nextEditUri;914} else {915return;916}917918// Make sure the completion list will not be disposed before the text change is sent.919completion.addRef();920921try {922let followUpTrigger = false;923editor.pushUndoStop();924if (isNextEditUri) {925// Do nothing926} else if (completion.action?.kind === 'edit') {927const action = completion.action;928if (alternativeAction && action.alternativeAction) {929followUpTrigger = true;930const altCommand = action.alternativeAction.command;931await this._commandService932.executeCommand(altCommand.id, ...(altCommand.arguments || []))933.then(undefined, onUnexpectedExternalError);934} else if (action.snippetInfo) {935const mainEdit = TextReplacement.delete(action.textReplacement.range);936const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));937const edit = TextEdit.fromParallelReplacementsUnsorted([mainEdit, ...additionalEdits]);938editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId()));939940editor.setPosition(action.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept');941SnippetController2.get(editor)?.insert(action.snippetInfo.snippet, { undoStopBefore: false });942} else {943const edits = state.edits;944945// The cursor should move to the end of the edit, not the end of the range provided by the extension946// Inline Edit diffs (human readable) the suggestion from the extension so it already removes common suffix/prefix947// Inline Completions does diff the suggestion so it may contain common suffix948let minimalEdits = edits;949if (state.kind === 'ghostText') {950minimalEdits = removeTextReplacementCommonSuffixPrefix(edits, this.textModel);951}952const selections = getEndPositionsAfterApplying(minimalEdits).map(p => Selection.fromPositions(p));953954const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));955const edit = TextEdit.fromParallelReplacementsUnsorted([...edits, ...additionalEdits]);956957editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId()));958959if (completion.hint === undefined) {960// do not move the cursor when the completion is displayed in a different location961editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept');962}963964if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) {965const editRanges = edit.getNewRanges();966const dec = this._store.add(new FadeoutDecoration(editor, editRanges, () => {967this._store.delete(dec);968}));969}970}971}972973this._onDidAccept.fire();974975// Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset).976this.stop();977978if (completion.command) {979await this._commandService980.executeCommand(completion.command.id, ...(completion.command.arguments || []))981.then(undefined, onUnexpectedExternalError);982}983984// TODO: how can we make alternative actions to retrigger?985if (followUpTrigger) {986this.trigger(undefined);987}988989completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction });990} finally {991completion.removeRef();992this._inAcceptFlow.set(true, undefined);993this._lastAcceptedInlineCompletionInfo = { textModelVersionIdAfter: this.textModel.getVersionId(), inlineCompletion: completion };994}995}996997public async acceptNextWord(): Promise<void> {998await this._acceptNext(this._editor, 'word', (pos, text) => {999const langId = this.textModel.getLanguageIdAtPosition(pos.lineNumber, pos.column);1000const config = this._languageConfigurationService.getLanguageConfiguration(langId);1001const wordRegExp = new RegExp(config.wordDefinition.source, config.wordDefinition.flags.replace('g', ''));10021003const m1 = text.match(wordRegExp);1004let acceptUntilIndexExclusive = 0;1005if (m1 && m1.index !== undefined) {1006if (m1.index === 0) {1007acceptUntilIndexExclusive = m1[0].length;1008} else {1009acceptUntilIndexExclusive = m1.index;1010}1011} else {1012acceptUntilIndexExclusive = text.length;1013}10141015const wsRegExp = /\s+/g;1016const m2 = wsRegExp.exec(text);1017if (m2 && m2.index !== undefined) {1018if (m2.index + m2[0].length < acceptUntilIndexExclusive) {1019acceptUntilIndexExclusive = m2.index + m2[0].length;1020}1021}1022return acceptUntilIndexExclusive;1023}, PartialAcceptTriggerKind.Word);1024}10251026public async acceptNextLine(): Promise<void> {1027await this._acceptNext(this._editor, 'line', (pos, text) => {1028const m = text.match(/\n/);1029if (m && m.index !== undefined) {1030return m.index + 1;1031}1032return text.length;1033}, PartialAcceptTriggerKind.Line);1034}10351036private async _acceptNext(editor: ICodeEditor, type: 'word' | 'line', getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise<void> {1037if (editor.getModel() !== this.textModel) {1038throw new BugIndicatingError();1039}10401041const state = this.inlineCompletionState.get();1042if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) {1043return;1044}1045const ghostText = state.primaryGhostText;1046const completion = state.inlineSuggestion;10471048if (completion.snippetInfo) {1049// not in WYSIWYG mode, partial commit might change completion, thus it is not supported1050await this.accept(editor);1051return;1052}10531054const firstPart = ghostText.parts[0];1055const ghostTextPos = new Position(ghostText.lineNumber, firstPart.column);1056const ghostTextVal = firstPart.text;1057const acceptUntilIndexExclusive = getAcceptUntilIndex(ghostTextPos, ghostTextVal);1058if (acceptUntilIndexExclusive === ghostTextVal.length && ghostText.parts.length === 1) {1059this.accept(editor);1060return;1061}1062const partialGhostTextVal = ghostTextVal.substring(0, acceptUntilIndexExclusive);10631064const positions = this._positions.get();1065const cursorPosition = positions[0];10661067// Executing the edit might free the completion, so we have to hold a reference on it.1068completion.addRef();1069try {1070this._isAcceptingPartially = true;1071try {1072editor.pushUndoStop();1073const replaceRange = Range.fromPositions(cursorPosition, ghostTextPos);1074const newText = editor.getModel()!.getValueInRange(replaceRange) + partialGhostTextVal;1075const primaryEdit = new TextReplacement(replaceRange, newText);1076const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)].filter(isDefined);1077const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p));10781079editor.edit(TextEdit.fromParallelReplacementsUnsorted(edits), this._getMetadata(completion, type));1080editor.setSelections(selections, 'inlineCompletionPartialAccept');1081editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Smooth);1082} finally {1083this._isAcceptingPartially = false;1084}10851086const acceptedRange = Range.fromPositions(completion.editRange.getStartPosition(), TextLength.ofText(partialGhostTextVal).addToPosition(ghostTextPos));1087// This assumes that the inline completion and the model use the same EOL style.1088const text = editor.getModel()!.getValueInRange(acceptedRange, EndOfLinePreference.LF);1089const acceptedLength = text.length;1090completion.reportPartialAccept(1091acceptedLength,1092{ kind, acceptedLength: acceptedLength },1093{ characters: acceptUntilIndexExclusive, ratio: acceptUntilIndexExclusive / ghostTextVal.length, count: 1 }1094);10951096} finally {1097completion.removeRef();1098}1099}11001101public handleSuggestAccepted(item: SuggestItemInfo) {1102const itemEdit = singleTextRemoveCommonPrefix(item.getSingleTextEdit(), this.textModel);1103const augmentedCompletion = this._computeAugmentation(itemEdit, undefined);1104if (!augmentedCompletion) { return; }11051106// This assumes that the inline completion and the model use the same EOL style.1107const alreadyAcceptedLength = this.textModel.getValueInRange(augmentedCompletion.completion.editRange, EndOfLinePreference.LF).length;1108const acceptedLength = alreadyAcceptedLength + itemEdit.text.length;11091110augmentedCompletion.completion.reportPartialAccept(itemEdit.text.length, {1111kind: PartialAcceptTriggerKind.Suggest,1112acceptedLength,1113}, {1114characters: itemEdit.text.length,1115count: 1,1116ratio: 11117});1118}11191120public extractReproSample(): Repro {1121const value = this.textModel.getValue();1122const item = this.state.get()?.inlineSuggestion;1123return {1124documentValue: value,1125inlineCompletion: item?.getSourceCompletion(),1126};1127}11281129private readonly _jumpedToId = observableValue<undefined | string>(this, undefined);1130private readonly _inAcceptFlow = observableValue(this, false);1131public readonly inAcceptFlow: IObservable<boolean> = this._inAcceptFlow;11321133public jump(): void {1134const s = this.inlineEditState.get();1135if (!s) { return; }11361137const suggestion = s.inlineSuggestion;1138suggestion.addRef();1139try {1140transaction(tx => {1141if (suggestion.action?.kind === 'jumpTo') {1142this.stop(undefined, tx);1143suggestion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction: false });1144}11451146this._jumpedToId.set(s.inlineSuggestion.semanticId, tx);1147this.dontRefetchSignal.trigger(tx);1148const targetRange = s.inlineSuggestion.targetRange;1149const targetPosition = targetRange.getStartPosition();1150this._editor.setPosition(targetPosition, 'inlineCompletions.jump');11511152// TODO: consider using view information to reveal it1153const isSingleLineChange = targetRange.isSingleLine() && (s.inlineSuggestion.hint || (s.inlineSuggestion.action?.kind === 'edit' && !s.inlineSuggestion.action.textReplacement.text.includes('\n')));1154if (isSingleLineChange || s.inlineSuggestion.action?.kind === 'jumpTo') {1155this._editor.revealPosition(targetPosition, ScrollType.Smooth);1156} else {1157const revealRange = new Range(targetRange.startLineNumber - 1, 1, targetRange.endLineNumber + 1, 1);1158this._editor.revealRange(revealRange, ScrollType.Smooth);1159}11601161s.inlineSuggestion.identity.setJumpTo(tx);11621163this._editor.focus();1164});1165} finally {1166suggestion.removeRef();1167}1168}11691170public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, timeWhenShown: number): Promise<void> {1171await inlineCompletion.reportInlineEditShown(this._commandService, viewKind, viewData, this.textModel, timeWhenShown);1172}1173}11741175interface Repro {1176documentValue: string;1177inlineCompletion: InlineCompletion | undefined;1178}11791180export enum VersionIdChangeReason {1181Undo,1182Redo,1183AcceptWord,1184Other,1185}11861187export function getSecondaryEdits(textModel: ITextModel, positions: readonly Position[], primaryTextRepl: TextReplacement): (TextReplacement | undefined)[] {1188if (positions.length === 1) {1189// No secondary cursor positions1190return [];1191}1192const text = new TextModelText(textModel);1193const textTransformer = text.getTransformer();1194const primaryOffset = textTransformer.getOffset(positions[0]);1195const secondaryOffsets = positions.slice(1).map(pos => textTransformer.getOffset(pos));11961197primaryTextRepl = primaryTextRepl.removeCommonPrefixAndSuffix(text);1198const primaryStringRepl = textTransformer.getStringReplacement(primaryTextRepl);11991200const deltaFromOffsetToRangeStart = primaryStringRepl.replaceRange.start - primaryOffset;1201const primaryContextRange = primaryStringRepl.replaceRange.join(OffsetRange.emptyAt(primaryOffset));1202const primaryContextValue = text.getValueOfOffsetRange(primaryContextRange);12031204const replacements = secondaryOffsets.map(secondaryOffset => {1205const newRangeStart = secondaryOffset + deltaFromOffsetToRangeStart;1206const newRangeEnd = newRangeStart + primaryStringRepl.replaceRange.length;1207const range = new OffsetRange(newRangeStart, newRangeEnd);12081209const contextRange = range.join(OffsetRange.emptyAt(secondaryOffset));1210const contextValue = text.getValueOfOffsetRange(contextRange);1211if (contextValue !== primaryContextValue) {1212return undefined;1213}12141215const stringRepl = new StringReplacement(range, primaryStringRepl.newText);1216const repl = textTransformer.getTextReplacement(stringRepl);1217return repl;1218}).filter(isDefined);12191220return replacements;1221}12221223class FadeoutDecoration extends Disposable {1224constructor(1225editor: ICodeEditor,1226ranges: Range[],1227onDispose?: () => void,1228) {1229super();12301231if (onDispose) {1232this._register({ dispose: () => onDispose() });1233}12341235this._register(observableCodeEditor(editor).setDecorations(constObservable(ranges.map<IModelDeltaDecoration>(range => ({1236range: range,1237options: {1238description: 'animation',1239className: 'edits-fadeout-decoration',1240zIndex: 1,1241}1242})))));12431244const animation = new AnimatedValue(1, 0, 1000, easeOutCubic);1245const val = new ObservableAnimatedValue(animation);12461247this._register(autorun(reader => {1248const opacity = val.getValue(reader);1249editor.getContainerDomNode().style.setProperty('--animation-opacity', opacity.toString());1250if (animation.isFinished()) {1251this.dispose();1252}1253}));1254}1255}12561257export function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem, reader: IReader | undefined = undefined): boolean {1258const targetRange = suggestion.targetRange;12591260// TODO make getVisibleRanges reactive!1261observableCodeEditor(editor).scrollTop.read(reader);1262const visibleRanges = editor.getVisibleRanges();12631264if (visibleRanges.length < 1) {1265return false;1266}12671268const viewportRange = new Range(1269visibleRanges[0].startLineNumber,1270visibleRanges[0].startColumn,1271visibleRanges[visibleRanges.length - 1].endLineNumber,1272visibleRanges[visibleRanges.length - 1].endColumn1273);1274return viewportRange.containsRange(targetRange);1275}12761277function skuFromAccount(account: IDefaultAccount | null): InlineSuggestSku | undefined {1278if (account?.access_type_sku && account?.copilot_plan) {1279return { type: account.access_type_sku, plan: account.copilot_plan };1280}1281return undefined;1282}12831284class DisposableCallback<T> {1285private _cb: ((e: T) => void) | undefined;12861287constructor(cb: (e: T) => void) {1288this._cb = cb;1289}12901291dispose(): void {1292this._cb = undefined;1293}12941295readonly handler = (val: T) => {1296return this._cb?.(val);1297};1298}12991300function createDisposableCb<T>(cb: (e: T) => void, store: DisposableStore): (e: T) => void {1301const dcb = new DisposableCallback(cb);1302store.add(dcb);1303return dcb.handler;1304}130513061307