Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts
5281 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BugIndicatingError } from '../../../../../base/common/errors.js';6import { IObservable, ITransaction, observableSignal, observableValue } from '../../../../../base/common/observable.js';7import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js';8import { ICommandService } from '../../../../../platform/commands/common/commands.js';9import { ISingleEditOperation } from '../../../../common/core/editOperation.js';10import { applyEditsToRanges, StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js';11import { TextEdit, TextReplacement } from '../../../../common/core/edits/textEdit.js';12import { Position } from '../../../../common/core/position.js';13import { Range } from '../../../../common/core/range.js';14import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';15import { StringText } from '../../../../common/core/text/abstractText.js';16import { getPositionOffsetTransformerFromTextModel } from '../../../../common/core/text/getPositionOffsetTransformerFromTextModel.js';17import { PositionOffsetTransformerBase } from '../../../../common/core/text/positionToOffset.js';18import { TextLength } from '../../../../common/core/text/textLength.js';19import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js';20import { Command, IInlineCompletionHint, InlineCompletion, InlineCompletionEndOfLifeReason, InlineCompletionHintStyle, InlineCompletionTriggerKind, InlineCompletionWarning, PartialAcceptInfo } from '../../../../common/languages.js';21import { ITextModel } from '../../../../common/model.js';22import { TextModelText } from '../../../../common/model/textModelText.js';23import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';24import { computeEditKind, InlineSuggestionEditKind } from './editKind.js';25import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js';26import { IInlineSuggestDataAction, IInlineSuggestDataActionEdit, InlineSuggestData, InlineSuggestionList, PartialAcceptance, RenameInfo, SnippetInfo } from './provideInlineCompletions.js';27import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js';28import { TextModelValueReference } from './textModelValueReference.js';2930export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem;3132export namespace InlineSuggestionItem {33export function create(34data: InlineSuggestData,35textModel: TextModelValueReference,36shouldDiffEdit: boolean = true, // TODO@benibenj it should only be created once and hence not meeded to be passed here37): InlineSuggestionItem {38if (!data.isInlineEdit && !data.action?.uri && data.action?.kind === 'edit') {39return InlineCompletionItem.create(data, textModel, data.action);40} else {41return InlineEditItem.create(data, textModel, shouldDiffEdit);42}43}44}4546export type InlineSuggestionAction = IInlineSuggestionActionEdit | IInlineSuggestionActionJumpTo;4748export interface IInlineSuggestionActionEdit {49kind: 'edit';50textReplacement: TextReplacement;51snippetInfo: SnippetInfo | undefined;52stringEdit: StringEdit;53target: TextModelValueReference;54alternativeAction: InlineSuggestAlternativeAction | undefined;55}5657export interface IInlineSuggestionActionJumpTo {58kind: 'jumpTo';59position: Position;60offset: number;61target: TextModelValueReference;62}6364function hashInlineSuggestionAction(action: InlineSuggestionAction | undefined): string {65const obj = action?.kind === 'edit' ? {66...action, alternativeAction: InlineSuggestAlternativeAction.toString(action.alternativeAction),67target: action?.target.uri.toString(),68} : {69...action,70target: action?.target.uri.toString(),71};7273return JSON.stringify(obj);74}7576abstract class InlineSuggestionItemBase {77constructor(78protected readonly _data: InlineSuggestData,79public readonly identity: InlineSuggestionIdentity,80public readonly hint: InlineSuggestHint | undefined,81/**82* Reference to the text model this item targets.83* For cross-file edits, this may differ from the current editor's model.84*/85public readonly originalTextRef: TextModelValueReference,86) {87}8889public abstract get action(): InlineSuggestionAction | undefined;9091/**92* A reference to the original inline completion list this inline completion has been constructed from.93* Used for event data to ensure referential equality.94*/95public get source(): InlineSuggestionList { return this._data.source; }9697public get isFromExplicitRequest(): boolean { return this._data.context.triggerKind === InlineCompletionTriggerKind.Explicit; }98public get forwardStable(): boolean { return this.source.inlineSuggestions.enableForwardStability ?? false; }99100public get targetRange(): Range {101if (this.hint) {102return this.hint.range;103}104if (this.action?.kind === 'edit') {105return this.action.textReplacement.range;106} else if (this.action?.kind === 'jumpTo') {107return Range.fromPositions(this.action.position);108}109throw new BugIndicatingError('InlineSuggestionItem: Either hint or action must be set');110}111112public get semanticId(): string { return this.hash; }113public get gutterMenuLinkAction(): Command | undefined { return this._sourceInlineCompletion.gutterMenuLinkAction; }114public get command(): Command | undefined { return this._sourceInlineCompletion.command; }115public get supportsRename(): boolean { return this._data.supportsRename; }116public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; }117public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; }118public get hash(): string {119return hashInlineSuggestionAction(this.action);120}121/** @deprecated */122public get shownCommand(): Command | undefined { return this._sourceInlineCompletion.shownCommand; }123124public get requestUuid(): string { return this._data.context.requestUuid; }125126public get partialAccepts(): PartialAcceptance { return this._data.partialAccepts; }127128/**129* A reference to the original inline completion this inline completion has been constructed from.130* Used for event data to ensure referential equality.131*/132private get _sourceInlineCompletion(): InlineCompletion { return this._data.sourceInlineCompletion; }133134135public abstract withEdit(userEdit: StringEdit, textModel: ITextModel): InlineSuggestionItem | undefined;136137public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem;138public abstract canBeReused(model: ITextModel, position: Position): boolean;139140public abstract computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined;141142public addRef(): void {143this.identity.addRef();144this.source.addRef();145}146147public removeRef(): void {148this.identity.removeRef();149this.source.removeRef();150}151152public reportInlineEditShown(commandService: ICommandService, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, model: ITextModel, timeWhenShown: number) {153const insertText = this.action?.kind === 'edit' ? this.action.textReplacement.text : ''; // TODO@hediet support insertText === undefined154this._data.reportInlineEditShown(commandService, insertText, viewKind, viewData, this.computeEditKind(model), timeWhenShown);155}156157public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo, partialAcceptance: PartialAcceptance) {158this._data.reportPartialAccept(acceptedCharacters, info, partialAcceptance);159}160161public reportEndOfLife(reason: InlineCompletionEndOfLifeReason): void {162this._data.reportEndOfLife(reason);163}164165public setEndOfLifeReason(reason: InlineCompletionEndOfLifeReason): void {166this._data.setEndOfLifeReason(reason);167}168169public setIsPreceeded(item: InlineSuggestionItem): void {170this._data.setIsPreceeded(item.partialAccepts);171}172173public setNotShownReasonIfNotSet(reason: string): void {174this._data.setNotShownReason(reason);175}176177/**178* Avoid using this method. Instead introduce getters for the needed properties.179*/180public getSourceCompletion(): InlineCompletion {181return this._sourceInlineCompletion;182}183184public setRenameProcessingInfo(info: RenameInfo): void {185this._data.setRenameProcessingInfo(info);186}187188public withAction(action: IInlineSuggestDataAction): InlineSuggestData {189return this._data.withAction(action);190}191192public addPerformanceMarker(marker: string): void {193this._data.addPerformanceMarker(marker);194}195}196197export class InlineSuggestionIdentity {198private static idCounter = 0;199private readonly _onDispose = observableSignal(this);200public readonly onDispose: IObservable<void> = this._onDispose;201202private readonly _jumpedTo = observableValue(this, false);203public get jumpedTo(): IObservable<boolean> {204return this._jumpedTo;205}206207private _refCount = 0;208public readonly id = 'InlineCompletionIdentity' + InlineSuggestionIdentity.idCounter++;209210addRef() {211this._refCount++;212}213214removeRef() {215this._refCount--;216if (this._refCount === 0) {217this._onDispose.trigger(undefined);218}219}220221setJumpTo(tx: ITransaction | undefined): void {222this._jumpedTo.set(true, tx);223}224}225226export class InlineSuggestHint {227228public static create(hint: IInlineCompletionHint) {229return new InlineSuggestHint(230Range.lift(hint.range),231hint.content,232hint.style,233);234}235236private constructor(237public readonly range: Range,238public readonly content: string,239public readonly style: InlineCompletionHintStyle,240) { }241242public withEdit(edit: StringEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestHint | undefined {243const offsetRange = new OffsetRange(244positionOffsetTransformer.getOffset(this.range.getStartPosition()),245positionOffsetTransformer.getOffset(this.range.getEndPosition())246);247248const newOffsetRange = applyEditsToRanges([offsetRange], edit)[0];249if (!newOffsetRange) {250return undefined;251}252253const newRange = positionOffsetTransformer.getRange(newOffsetRange);254255return new InlineSuggestHint(newRange, this.content, this.style);256}257}258259export class InlineCompletionItem extends InlineSuggestionItemBase {260public static create(261data: InlineSuggestData,262textModel: TextModelValueReference,263action: IInlineSuggestDataActionEdit,264): InlineCompletionItem {265const identity = new InlineSuggestionIdentity();266const transformer = textModel.getTransformer();267268const insertText = action.insertText.replace(/\r\n|\r|\n/g, textModel.getEOL());269270const edit = reshapeInlineCompletion(new StringReplacement(transformer.getOffsetRange(action.range), insertText), textModel);271const trimmedEdit = edit.removeCommonSuffixAndPrefix(textModel.getValue());272const textEdit = transformer.getTextReplacement(edit);273274const displayLocation = data.hint ? InlineSuggestHint.create(data.hint) : undefined;275276return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, action.snippetInfo, data.additionalTextEdits, data, identity, displayLocation, textModel);277}278279public readonly isInlineEdit = false;280281private constructor(282private readonly _edit: StringReplacement,283private readonly _trimmedEdit: StringReplacement,284private readonly _textEdit: TextReplacement,285private readonly _originalRange: Range,286public readonly snippetInfo: SnippetInfo | undefined,287public readonly additionalTextEdits: readonly ISingleEditOperation[],288289data: InlineSuggestData,290identity: InlineSuggestionIdentity,291displayLocation: InlineSuggestHint | undefined,292originalTextRef: TextModelValueReference,293) {294super(data, identity, displayLocation, originalTextRef);295}296297override get action(): IInlineSuggestionActionEdit {298return {299kind: 'edit',300textReplacement: this.getSingleTextEdit(),301snippetInfo: this.snippetInfo,302stringEdit: new StringEdit([this._trimmedEdit]),303alternativeAction: undefined,304target: this.originalTextRef,305};306}307308override get hash(): string {309return JSON.stringify(this._trimmedEdit.toJson());310}311312getSingleTextEdit(): TextReplacement { return this._textEdit; }313314override withIdentity(identity: InlineSuggestionIdentity): InlineCompletionItem {315return new InlineCompletionItem(316this._edit,317this._trimmedEdit,318this._textEdit,319this._originalRange,320this.snippetInfo,321this.additionalTextEdits,322this._data,323identity,324this.hint,325this.originalTextRef326);327}328329override withEdit(textModelEdit: StringEdit, textModel: ITextModel): InlineCompletionItem | undefined {330// If the edit is to a different model than our target, it's a noop331if (!this.originalTextRef.targets(textModel)) {332return this; // unchanged333}334335const newEditRange = applyEditsToRanges([this._edit.replaceRange], textModelEdit);336if (newEditRange.length === 0) {337return undefined;338}339const newEdit = new StringReplacement(newEditRange[0], this._textEdit.text);340const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel);341const newTextEdit = positionOffsetTransformer.getTextReplacement(newEdit);342343let newDisplayLocation = this.hint;344if (newDisplayLocation) {345newDisplayLocation = newDisplayLocation.withEdit(textModelEdit, positionOffsetTransformer);346if (!newDisplayLocation) {347return undefined;348}349}350351const trimmedEdit = newEdit.removeCommonSuffixAndPrefix(textModel.getValue());352353return new InlineCompletionItem(354newEdit,355trimmedEdit,356newTextEdit,357this._originalRange,358this.snippetInfo,359this.additionalTextEdits,360this._data,361this.identity,362newDisplayLocation,363this.originalTextRef364);365}366367override canBeReused(model: ITextModel, position: Position): boolean {368// TODO@hediet I believe this can be simplified to `return true;`, as applying an edit should kick out this suggestion.369const updatedRange = this._textEdit.range;370const result = !!updatedRange371&& updatedRange.containsPosition(position)372&& this.isVisible(model, position)373&& TextLength.ofRange(updatedRange).isGreaterThanOrEqualTo(TextLength.ofRange(this._originalRange));374return result;375}376377public isVisible(model: ITextModel, cursorPosition: Position): boolean {378const singleTextEdit = this.getSingleTextEdit();379return inlineCompletionIsVisible(singleTextEdit, this._originalRange, model, cursorPosition);380}381382override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined {383return computeEditKind(new StringEdit([this._edit]), model);384}385386public get editRange(): Range { return this.getSingleTextEdit().range; }387public get insertText(): string { return this.getSingleTextEdit().text; }388}389390export class InlineEditItem extends InlineSuggestionItemBase {391public static createForTest(392textModel: TextModelValueReference,393range: Range,394newText: string,395): InlineEditItem {396const action: IInlineSuggestDataAction = {397kind: 'edit',398snippetInfo: undefined,399insertText: newText,400range: range,401uri: textModel.uri,402alternativeAction: undefined,403};404405return InlineEditItem.create(InlineSuggestData.createForTest(action, textModel.uri), textModel);406}407408public static create(409data: InlineSuggestData,410textModel: TextModelValueReference,411shouldDiffEdit: boolean = true,412): InlineEditItem {413let action: InlineSuggestionAction | undefined;414let edits: SingleUpdatedNextEdit[] = [];415if (data.action?.kind === 'edit') {416const offsetEdit = shouldDiffEdit ? getDiffedStringEdit(textModel, data.action.range, data.action.insertText) : getStringEdit(textModel, data.action.range, data.action.insertText); // TODO compute async417const textEdit = TextEdit.fromStringEdit(offsetEdit, textModel);418const singleTextEdit = offsetEdit.isEmpty() ? new TextReplacement(new Range(1, 1, 1, 1), '') : textEdit.toReplacement(textModel); // FIXME: .toReplacement() can throw because offsetEdit is empty because we get an empty diff in getStringEdit after diffing419420edits = offsetEdit.replacements.map(edit => {421const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getTransformer().getPosition(edit.replaceRange.endExclusive));422const replacedText = textModel.getValueInRange(replacedRange);423return SingleUpdatedNextEdit.create(edit, replacedText);424});425426action = {427kind: 'edit',428snippetInfo: data.action.snippetInfo,429stringEdit: offsetEdit,430textReplacement: singleTextEdit,431alternativeAction: data.action.alternativeAction,432target: textModel,433};434} else if (data.action?.kind === 'jumpTo') {435action = {436kind: 'jumpTo',437position: data.action.position,438offset: textModel.getTransformer().getOffset(data.action.position),439target: textModel,440};441} else {442action = undefined;443if (!data.hint) {444throw new BugIndicatingError('InlineEditItem: action is undefined and no hint is provided');445}446}447448const identity = new InlineSuggestionIdentity();449450const hint = data.hint ? InlineSuggestHint.create(data.hint) : undefined;451return new InlineEditItem(action, data, identity, edits, hint, false, textModel.getVersionId(), textModel);452}453454public readonly snippetInfo: SnippetInfo | undefined = undefined;455public readonly additionalTextEdits: readonly ISingleEditOperation[] = [];456public readonly isInlineEdit = true;457458private constructor(459private readonly _action: InlineSuggestionAction | undefined,460461data: InlineSuggestData,462463identity: InlineSuggestionIdentity,464private readonly _edits: readonly SingleUpdatedNextEdit[],465hint: InlineSuggestHint | undefined,466private readonly _lastChangePartOfInlineEdit = false,467private readonly _inlineEditModelVersion: number,468originalTextRef: TextModelValueReference,469) {470super(data, identity, hint, originalTextRef);471}472473public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; }474// public get updatedEdit(): StringEdit { return this._edit; }475476override get action(): InlineSuggestionAction | undefined {477return this._action;478}479480override withIdentity(identity: InlineSuggestionIdentity): InlineEditItem {481return new InlineEditItem(482this._action,483this._data,484identity,485this._edits,486this.hint,487this._lastChangePartOfInlineEdit,488this._inlineEditModelVersion,489this.originalTextRef,490);491}492493override canBeReused(model: ITextModel, position: Position): boolean {494// TODO@hediet I believe this can be simplified to `return true;`, as applying an edit should kick out this suggestion.495return this._lastChangePartOfInlineEdit && this.updatedEditModelVersion === model.getVersionId();496}497498override withEdit(textModelChanges: StringEdit, textModel: ITextModel): InlineEditItem | undefined {499// If the edit is to a different model than our target, it's a noop500if (!this.originalTextRef.targets(textModel)) {501return this; // unchanged502}503504const edit = this._applyTextModelChanges(textModelChanges, this._edits, textModel);505return edit;506}507508private _applyTextModelChanges(textModelChanges: StringEdit, edits: readonly SingleUpdatedNextEdit[], textModel: ITextModel): InlineEditItem | undefined {509const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel);510511let lastChangePartOfInlineEdit = false;512let inlineEditModelVersion = this._inlineEditModelVersion;513let newAction: InlineSuggestionAction | undefined;514515if (this.action?.kind === 'edit') { // TODO What about rename?516edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges));517518if (edits.some(edit => edit.edit === undefined)) {519return undefined; // change is invalid, so we will have to drop the completion520}521522523const newTextModelVersion = textModel.getVersionId();524lastChangePartOfInlineEdit = edits.some(edit => edit.lastChangeUpdatedEdit);525if (lastChangePartOfInlineEdit) {526inlineEditModelVersion = newTextModelVersion ?? -1;527}528529if (newTextModelVersion === null || inlineEditModelVersion + 20 < newTextModelVersion) {530return undefined; // the completion has been ignored for a while, remove it531}532533edits = edits.filter(innerEdit => !innerEdit.edit!.isEmpty);534if (edits.length === 0) {535return undefined; // the completion has been typed by the user536}537538const newEdit = new StringEdit(edits.map(edit => edit.edit!));539540const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toReplacement(new TextModelText(textModel));541542newAction = {543kind: 'edit',544textReplacement: newTextEdit,545snippetInfo: this.snippetInfo,546stringEdit: newEdit,547alternativeAction: this.action.alternativeAction,548target: this.originalTextRef,549};550} else if (this.action?.kind === 'jumpTo') {551const jumpToOffset = this.action.offset;552const newJumpToOffset = textModelChanges.applyToOffsetOrUndefined(jumpToOffset);553if (newJumpToOffset === undefined) {554return undefined;555}556const newJumpToPosition = positionOffsetTransformer.getPosition(newJumpToOffset);557558newAction = {559kind: 'jumpTo',560position: newJumpToPosition,561offset: newJumpToOffset,562target: this.originalTextRef,563};564} else {565newAction = undefined;566}567568let newDisplayLocation = this.hint;569if (newDisplayLocation) {570newDisplayLocation = newDisplayLocation.withEdit(textModelChanges, positionOffsetTransformer);571if (!newDisplayLocation) {572return undefined;573}574}575576return new InlineEditItem(577newAction,578this._data,579this.identity,580edits,581newDisplayLocation,582lastChangePartOfInlineEdit,583inlineEditModelVersion,584this.originalTextRef,585);586}587588override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined {589const edit = this.action?.kind === 'edit' ? this.action.stringEdit : undefined;590if (!edit) {591return undefined;592}593return computeEditKind(edit, model);594}595}596597function getDiffedStringEdit(textModel: TextModelValueReference, editRange: Range, replaceText: string): StringEdit {598const eol = textModel.getEOL();599const editOriginalText = textModel.getValueOfRange(editRange);600const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol);601602const diffAlgorithm = linesDiffComputers.getDefault();603const lineDiffs = diffAlgorithm.computeDiff(604splitLines(editOriginalText),605splitLines(editReplaceText),606{607ignoreTrimWhitespace: false,608computeMoves: false,609extendToSubwords: true,610maxComputationTimeMs: 50,611}612);613614const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []);615616function addRangeToPos(pos: Position, range: Range): Range {617const start = TextLength.fromPosition(range.getStartPosition());618return TextLength.ofRange(range).createRange(start.addToPosition(pos));619}620621const modifiedText = new StringText(editReplaceText);622623const offsetEdit = new StringEdit(624innerChanges.map(c => {625const rangeInModel = addRangeToPos(editRange.getStartPosition(), c.originalRange);626const originalRange = textModel.getTransformer().getOffsetRange(rangeInModel);627628const replaceText = modifiedText.getValueOfRange(c.modifiedRange);629const edit = new StringReplacement(originalRange, replaceText);630631const originalText = textModel.getValueOfRange(rangeInModel);632return reshapeInlineEdit(edit, originalText, innerChanges.length, textModel);633})634);635636return offsetEdit;637}638639function getStringEdit(textModel: TextModelValueReference, editRange: Range, replaceText: string): StringEdit {640return new StringEdit([new StringReplacement(641textModel.getTransformer().getOffsetRange(editRange),642replaceText643)]);644}645646class SingleUpdatedNextEdit {647public static create(648edit: StringReplacement,649replacedText: string,650): SingleUpdatedNextEdit {651const prefixLength = commonPrefixLength(edit.newText, replacedText);652const suffixLength = commonSuffixLength(edit.newText, replacedText);653const trimmedNewText = edit.newText.substring(prefixLength, edit.newText.length - suffixLength);654return new SingleUpdatedNextEdit(edit, trimmedNewText, prefixLength, suffixLength);655}656657public get edit() { return this._edit; }658public get lastChangeUpdatedEdit() { return this._lastChangeUpdatedEdit; }659660constructor(661private _edit: StringReplacement | undefined,662private _trimmedNewText: string,663private _prefixLength: number,664private _suffixLength: number,665private _lastChangeUpdatedEdit: boolean = false,666) {667}668669public applyTextModelChanges(textModelChanges: StringEdit) {670const c = this._clone();671c._applyTextModelChanges(textModelChanges);672return c;673}674675private _clone(): SingleUpdatedNextEdit {676return new SingleUpdatedNextEdit(677this._edit,678this._trimmedNewText,679this._prefixLength,680this._suffixLength,681this._lastChangeUpdatedEdit,682);683}684685private _applyTextModelChanges(textModelChanges: StringEdit) {686this._lastChangeUpdatedEdit = false; // TODO @benibenj make immutable687688if (!this._edit) {689throw new BugIndicatingError('UpdatedInnerEdits: No edit to apply changes to');690}691692const result = this._applyChanges(this._edit, textModelChanges);693if (!result) {694this._edit = undefined;695return;696}697698this._edit = result.edit;699this._lastChangeUpdatedEdit = result.editHasChanged;700}701702private _applyChanges(edit: StringReplacement, textModelChanges: StringEdit): { edit: StringReplacement; editHasChanged: boolean } | undefined {703let editStart = edit.replaceRange.start;704let editEnd = edit.replaceRange.endExclusive;705let editReplaceText = edit.newText;706let editHasChanged = false;707708const shouldPreserveEditShape = this._prefixLength > 0 || this._suffixLength > 0;709710for (let i = textModelChanges.replacements.length - 1; i >= 0; i--) {711const change = textModelChanges.replacements[i];712713// INSERTIONS (only support inserting at start of edit)714const isInsertion = change.newText.length > 0 && change.replaceRange.isEmpty;715716if (isInsertion && !shouldPreserveEditShape && change.replaceRange.start === editStart && editReplaceText.startsWith(change.newText)) {717editStart += change.newText.length;718editReplaceText = editReplaceText.substring(change.newText.length);719editEnd += change.newText.length;720editHasChanged = true;721continue;722}723724if (isInsertion && shouldPreserveEditShape && change.replaceRange.start === editStart + this._prefixLength && this._trimmedNewText.startsWith(change.newText)) {725editEnd += change.newText.length;726editHasChanged = true;727this._prefixLength += change.newText.length;728this._trimmedNewText = this._trimmedNewText.substring(change.newText.length);729continue;730}731732// DELETIONS733const isDeletion = change.newText.length === 0 && change.replaceRange.length > 0;734if (isDeletion && change.replaceRange.start >= editStart + this._prefixLength && change.replaceRange.endExclusive <= editEnd - this._suffixLength) {735// user deleted text IN-BETWEEN the deletion range736editEnd -= change.replaceRange.length;737editHasChanged = true;738continue;739}740741// user did exactly the edit742if (change.equals(edit)) {743editHasChanged = true;744editStart = change.replaceRange.endExclusive;745editReplaceText = '';746continue;747}748749// MOVE EDIT750if (change.replaceRange.start > editEnd) {751// the change happens after the completion range752continue;753}754if (change.replaceRange.endExclusive < editStart) {755// the change happens before the completion range756editStart += change.newText.length - change.replaceRange.length;757editEnd += change.newText.length - change.replaceRange.length;758continue;759}760761// The change intersects the completion, so we will have to drop the completion762return undefined;763}764765// the resulting edit is a noop as the original and new text are the same766if (this._trimmedNewText.length === 0 && editStart + this._prefixLength === editEnd - this._suffixLength) {767return { edit: new StringReplacement(new OffsetRange(editStart + this._prefixLength, editStart + this._prefixLength), ''), editHasChanged: true };768}769770return { edit: new StringReplacement(new OffsetRange(editStart, editEnd), editReplaceText), editHasChanged };771}772}773774function reshapeInlineCompletion(edit: StringReplacement, textModel: TextModelValueReference): StringReplacement {775// If the insertion is a multi line insertion starting on the next line776// Move it forwards so that the multi line insertion starts on the current line777const eol = textModel.getEOL();778if (edit.replaceRange.isEmpty && edit.newText.includes(eol)) {779edit = reshapeMultiLineInsertion(edit, textModel);780}781782return edit;783}784785function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: TextModelValueReference): StringReplacement {786// TODO: EOL are not properly trimmed by the diffAlgorithm #12680787const eol = textModel.getEOL();788if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) {789edit = new StringReplacement(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length));790}791792// INSERTION793// If the insertion ends with a new line and is inserted at the start of a line which has text,794// we move the insertion to the end of the previous line if possible795if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) {796const startPosition = textModel.getTransformer().getPosition(edit.replaceRange.start);797const hasTextOnInsertionLine = textModel.getLineLength(startPosition.lineNumber) !== 0;798if (hasTextOnInsertionLine) {799edit = reshapeMultiLineInsertion(edit, textModel);800}801}802803// The diff algorithm extended a simple edit to the entire word804// shrink it back to a simple edit if it is deletion/insertion only805if (totalInnerEdits === 1) {806const prefixLength = commonPrefixLength(originalText, edit.newText);807const suffixLength = commonSuffixLength(originalText.slice(prefixLength), edit.newText.slice(prefixLength));808809// reshape it back to an insertion810if (prefixLength + suffixLength === originalText.length) {811return new StringReplacement(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength));812}813814// reshape it back to a deletion815if (prefixLength + suffixLength === edit.newText.length) {816return new StringReplacement(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), '');817}818}819820return edit;821}822823function reshapeMultiLineInsertion(edit: StringReplacement, textModel: TextModelValueReference): StringReplacement {824if (!edit.replaceRange.isEmpty) {825throw new BugIndicatingError('Unexpected original range');826}827828if (edit.replaceRange.start === 0) {829return edit;830}831832const eol = textModel.getEOL();833const startPosition = textModel.getTransformer().getPosition(edit.replaceRange.start);834const startColumn = startPosition.column;835const startLineNumber = startPosition.lineNumber;836837// If the insertion ends with a new line and is inserted at the start of a line which has text,838// we move the insertion to the end of the previous line if possible839if (startColumn === 1 && startLineNumber > 1 && edit.newText.endsWith(eol) && !edit.newText.startsWith(eol)) {840return new StringReplacement(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length));841}842843return edit;844}845846847