Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.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 { 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 { URI } from '../../../../../base/common/uri.js';9import { ICommandService } from '../../../../../platform/commands/common/commands.js';10import { ISingleEditOperation } from '../../../../common/core/editOperation.js';11import { applyEditsToRanges, StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js';12import { TextEdit, TextReplacement } from '../../../../common/core/edits/textEdit.js';13import { Position } from '../../../../common/core/position.js';14import { Range } from '../../../../common/core/range.js';15import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';16import { StringText } from '../../../../common/core/text/abstractText.js';17import { getPositionOffsetTransformerFromTextModel } from '../../../../common/core/text/getPositionOffsetTransformerFromTextModel.js';18import { PositionOffsetTransformerBase } from '../../../../common/core/text/positionToOffset.js';19import { TextLength } from '../../../../common/core/text/textLength.js';20import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js';21import { Command, IInlineCompletionHint, InlineCompletion, InlineCompletionEndOfLifeReason, InlineCompletionHintStyle, InlineCompletionTriggerKind, InlineCompletionWarning, PartialAcceptInfo } from '../../../../common/languages.js';22import { ITextModel } from '../../../../common/model.js';23import { TextModelText } from '../../../../common/model/textModelText.js';24import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';25import { computeEditKind, InlineSuggestionEditKind } from './editKind.js';26import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js';27import { IInlineSuggestDataAction, IInlineSuggestDataActionEdit, InlineSuggestData, InlineSuggestionList, PartialAcceptance, RenameInfo, SnippetInfo } from './provideInlineCompletions.js';28import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js';2930export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem;3132export namespace InlineSuggestionItem {33export function create(34data: InlineSuggestData,35textModel: ITextModel,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;53uri: URI | undefined;54alternativeAction: InlineSuggestAlternativeAction | undefined;55}5657export interface IInlineSuggestionActionJumpTo {58kind: 'jumpTo';59position: Position;60offset: number;61uri: URI | undefined;62}6364function hashInlineSuggestionAction(action: InlineSuggestionAction | undefined): string {65const obj = action?.kind === 'edit' ? { ...action, alternativeAction: InlineSuggestAlternativeAction.toString(action.alternativeAction) } : action;66return JSON.stringify(obj);67}6869abstract class InlineSuggestionItemBase {70constructor(71protected readonly _data: InlineSuggestData,72public readonly identity: InlineSuggestionIdentity,73public readonly hint: InlineSuggestHint | undefined,74) {75}7677public abstract get action(): InlineSuggestionAction | undefined;7879/**80* A reference to the original inline completion list this inline completion has been constructed from.81* Used for event data to ensure referential equality.82*/83public get source(): InlineSuggestionList { return this._data.source; }8485public get isFromExplicitRequest(): boolean { return this._data.context.triggerKind === InlineCompletionTriggerKind.Explicit; }86public get forwardStable(): boolean { return this.source.inlineSuggestions.enableForwardStability ?? false; }8788public get targetRange(): Range {89if (this.hint) {90return this.hint.range;91}92if (this.action?.kind === 'edit') {93return this.action.textReplacement.range;94} else if (this.action?.kind === 'jumpTo') {95return Range.fromPositions(this.action.position);96}97throw new BugIndicatingError('InlineSuggestionItem: Either hint or action must be set');98}99100public get semanticId(): string { return this.hash; }101public get gutterMenuLinkAction(): Command | undefined { return this._sourceInlineCompletion.gutterMenuLinkAction; }102public get command(): Command | undefined { return this._sourceInlineCompletion.command; }103public get supportsRename(): boolean { return this._data.supportsRename; }104public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; }105public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; }106public get hash(): string {107return hashInlineSuggestionAction(this.action);108}109/** @deprecated */110public get shownCommand(): Command | undefined { return this._sourceInlineCompletion.shownCommand; }111112public get requestUuid(): string { return this._data.context.requestUuid; }113114public get partialAccepts(): PartialAcceptance { return this._data.partialAccepts; }115116/**117* A reference to the original inline completion this inline completion has been constructed from.118* Used for event data to ensure referential equality.119*/120private get _sourceInlineCompletion(): InlineCompletion { return this._data.sourceInlineCompletion; }121122123public abstract withEdit(userEdit: StringEdit, textModel: ITextModel): InlineSuggestionItem | undefined;124125public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem;126public abstract canBeReused(model: ITextModel, position: Position): boolean;127128public abstract computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined;129130public addRef(): void {131this.identity.addRef();132this.source.addRef();133}134135public removeRef(): void {136this.identity.removeRef();137this.source.removeRef();138}139140public reportInlineEditShown(commandService: ICommandService, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, model: ITextModel, timeWhenShown: number) {141const insertText = this.action?.kind === 'edit' ? this.action.textReplacement.text : ''; // TODO@hediet support insertText === undefined142this._data.reportInlineEditShown(commandService, insertText, viewKind, viewData, this.computeEditKind(model), timeWhenShown);143}144145public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo, partialAcceptance: PartialAcceptance) {146this._data.reportPartialAccept(acceptedCharacters, info, partialAcceptance);147}148149public reportEndOfLife(reason: InlineCompletionEndOfLifeReason): void {150this._data.reportEndOfLife(reason);151}152153public setEndOfLifeReason(reason: InlineCompletionEndOfLifeReason): void {154this._data.setEndOfLifeReason(reason);155}156157public setIsPreceeded(item: InlineSuggestionItem): void {158this._data.setIsPreceeded(item.partialAccepts);159}160161public setNotShownReasonIfNotSet(reason: string): void {162this._data.setNotShownReason(reason);163}164165/**166* Avoid using this method. Instead introduce getters for the needed properties.167*/168public getSourceCompletion(): InlineCompletion {169return this._sourceInlineCompletion;170}171172public setRenameProcessingInfo(info: RenameInfo): void {173this._data.setRenameProcessingInfo(info);174}175176public withAction(action: IInlineSuggestDataAction): InlineSuggestData {177return this._data.withAction(action);178}179180public addPerformanceMarker(marker: string): void {181this._data.addPerformanceMarker(marker);182}183}184185export class InlineSuggestionIdentity {186private static idCounter = 0;187private readonly _onDispose = observableSignal(this);188public readonly onDispose: IObservable<void> = this._onDispose;189190private readonly _jumpedTo = observableValue(this, false);191public get jumpedTo(): IObservable<boolean> {192return this._jumpedTo;193}194195private _refCount = 1;196public readonly id = 'InlineCompletionIdentity' + InlineSuggestionIdentity.idCounter++;197198addRef() {199this._refCount++;200}201202removeRef() {203this._refCount--;204if (this._refCount === 0) {205this._onDispose.trigger(undefined);206}207}208209setJumpTo(tx: ITransaction | undefined): void {210this._jumpedTo.set(true, tx);211}212}213214export class InlineSuggestHint {215216public static create(hint: IInlineCompletionHint) {217return new InlineSuggestHint(218Range.lift(hint.range),219hint.content,220hint.style,221);222}223224private constructor(225public readonly range: Range,226public readonly content: string,227public readonly style: InlineCompletionHintStyle,228) { }229230public withEdit(edit: StringEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestHint | undefined {231const offsetRange = new OffsetRange(232positionOffsetTransformer.getOffset(this.range.getStartPosition()),233positionOffsetTransformer.getOffset(this.range.getEndPosition())234);235236const newOffsetRange = applyEditsToRanges([offsetRange], edit)[0];237if (!newOffsetRange) {238return undefined;239}240241const newRange = positionOffsetTransformer.getRange(newOffsetRange);242243return new InlineSuggestHint(newRange, this.content, this.style);244}245}246247export class InlineCompletionItem extends InlineSuggestionItemBase {248public static create(249data: InlineSuggestData,250textModel: ITextModel,251action: IInlineSuggestDataActionEdit,252): InlineCompletionItem {253const identity = new InlineSuggestionIdentity();254const transformer = getPositionOffsetTransformerFromTextModel(textModel);255256const insertText = action.insertText.replace(/\r\n|\r|\n/g, textModel.getEOL());257258const edit = reshapeInlineCompletion(new StringReplacement(transformer.getOffsetRange(action.range), insertText), textModel);259const trimmedEdit = edit.removeCommonSuffixAndPrefix(textModel.getValue());260const textEdit = transformer.getTextReplacement(edit);261262const displayLocation = data.hint ? InlineSuggestHint.create(data.hint) : undefined;263264return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, action.snippetInfo, data.additionalTextEdits, data, identity, displayLocation);265}266267public readonly isInlineEdit = false;268269private constructor(270private readonly _edit: StringReplacement,271private readonly _trimmedEdit: StringReplacement,272private readonly _textEdit: TextReplacement,273private readonly _originalRange: Range,274public readonly snippetInfo: SnippetInfo | undefined,275public readonly additionalTextEdits: readonly ISingleEditOperation[],276277data: InlineSuggestData,278identity: InlineSuggestionIdentity,279displayLocation: InlineSuggestHint | undefined,280) {281super(data, identity, displayLocation);282}283284override get action(): IInlineSuggestionActionEdit {285return {286kind: 'edit',287textReplacement: this.getSingleTextEdit(),288snippetInfo: this.snippetInfo,289stringEdit: new StringEdit([this._trimmedEdit]),290uri: undefined,291alternativeAction: undefined,292};293}294295override get hash(): string {296return JSON.stringify(this._trimmedEdit.toJson());297}298299getSingleTextEdit(): TextReplacement { return this._textEdit; }300301override withIdentity(identity: InlineSuggestionIdentity): InlineCompletionItem {302return new InlineCompletionItem(303this._edit,304this._trimmedEdit,305this._textEdit,306this._originalRange,307this.snippetInfo,308this.additionalTextEdits,309this._data,310identity,311this.hint312);313}314315override withEdit(textModelEdit: StringEdit, textModel: ITextModel): InlineCompletionItem | undefined {316const newEditRange = applyEditsToRanges([this._edit.replaceRange], textModelEdit);317if (newEditRange.length === 0) {318return undefined;319}320const newEdit = new StringReplacement(newEditRange[0], this._textEdit.text);321const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel);322const newTextEdit = positionOffsetTransformer.getTextReplacement(newEdit);323324let newDisplayLocation = this.hint;325if (newDisplayLocation) {326newDisplayLocation = newDisplayLocation.withEdit(textModelEdit, positionOffsetTransformer);327if (!newDisplayLocation) {328return undefined;329}330}331332const trimmedEdit = newEdit.removeCommonSuffixAndPrefix(textModel.getValue());333334return new InlineCompletionItem(335newEdit,336trimmedEdit,337newTextEdit,338this._originalRange,339this.snippetInfo,340this.additionalTextEdits,341this._data,342this.identity,343newDisplayLocation344);345}346347override canBeReused(model: ITextModel, position: Position): boolean {348// TODO@hediet I believe this can be simplified to `return true;`, as applying an edit should kick out this suggestion.349const updatedRange = this._textEdit.range;350const result = !!updatedRange351&& updatedRange.containsPosition(position)352&& this.isVisible(model, position)353&& TextLength.ofRange(updatedRange).isGreaterThanOrEqualTo(TextLength.ofRange(this._originalRange));354return result;355}356357public isVisible(model: ITextModel, cursorPosition: Position): boolean {358const singleTextEdit = this.getSingleTextEdit();359return inlineCompletionIsVisible(singleTextEdit, this._originalRange, model, cursorPosition);360}361362override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined {363return computeEditKind(new StringEdit([this._edit]), model);364}365366public get editRange(): Range { return this.getSingleTextEdit().range; }367public get insertText(): string { return this.getSingleTextEdit().text; }368}369370export class InlineEditItem extends InlineSuggestionItemBase {371public static create(372data: InlineSuggestData,373textModel: ITextModel,374shouldDiffEdit: boolean = true,375): InlineEditItem {376let action: InlineSuggestionAction | undefined;377let edits: SingleUpdatedNextEdit[] = [];378if (data.action?.kind === 'edit') {379const offsetEdit = shouldDiffEdit ? getDiffedStringEdit(textModel, data.action.range, data.action.insertText) : getStringEdit(textModel, data.action.range, data.action.insertText); // TODO compute async380const text = new TextModelText(textModel);381const textEdit = TextEdit.fromStringEdit(offsetEdit, text);382const singleTextEdit = offsetEdit.isEmpty() ? new TextReplacement(new Range(1, 1, 1, 1), '') : textEdit.toReplacement(text); // FIXME: .toReplacement() can throw because offsetEdit is empty because we get an empty diff in getStringEdit after diffing383384edits = offsetEdit.replacements.map(edit => {385const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getPositionAt(edit.replaceRange.endExclusive));386const replacedText = textModel.getValueInRange(replacedRange);387return SingleUpdatedNextEdit.create(edit, replacedText);388});389390action = {391kind: 'edit',392snippetInfo: data.action.snippetInfo,393stringEdit: offsetEdit,394textReplacement: singleTextEdit,395uri: data.action.uri,396alternativeAction: data.action.alternativeAction,397};398} else if (data.action?.kind === 'jumpTo') {399action = {400kind: 'jumpTo',401position: data.action.position,402offset: textModel.getOffsetAt(data.action.position),403uri: data.action.uri,404};405} else {406action = undefined;407if (!data.hint) {408throw new BugIndicatingError('InlineEditItem: action is undefined and no hint is provided');409}410}411412const identity = new InlineSuggestionIdentity();413414const hint = data.hint ? InlineSuggestHint.create(data.hint) : undefined;415return new InlineEditItem(action, data, identity, edits, hint, false, textModel.getVersionId());416}417418public readonly snippetInfo: SnippetInfo | undefined = undefined;419public readonly additionalTextEdits: readonly ISingleEditOperation[] = [];420public readonly isInlineEdit = true;421422private constructor(423private readonly _action: InlineSuggestionAction | undefined,424425data: InlineSuggestData,426427identity: InlineSuggestionIdentity,428private readonly _edits: readonly SingleUpdatedNextEdit[],429hint: InlineSuggestHint | undefined,430private readonly _lastChangePartOfInlineEdit = false,431private readonly _inlineEditModelVersion: number,432) {433super(data, identity, hint);434}435436public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; }437// public get updatedEdit(): StringEdit { return this._edit; }438439override get action(): InlineSuggestionAction | undefined {440return this._action;441}442443override withIdentity(identity: InlineSuggestionIdentity): InlineEditItem {444return new InlineEditItem(445this._action,446this._data,447identity,448this._edits,449this.hint,450this._lastChangePartOfInlineEdit,451this._inlineEditModelVersion,452);453}454455override canBeReused(model: ITextModel, position: Position): boolean {456// TODO@hediet I believe this can be simplified to `return true;`, as applying an edit should kick out this suggestion.457return this._lastChangePartOfInlineEdit && this.updatedEditModelVersion === model.getVersionId();458}459460override withEdit(textModelChanges: StringEdit, textModel: ITextModel): InlineEditItem | undefined {461const edit = this._applyTextModelChanges(textModelChanges, this._edits, textModel);462return edit;463}464465private _applyTextModelChanges(textModelChanges: StringEdit, edits: readonly SingleUpdatedNextEdit[], textModel: ITextModel): InlineEditItem | undefined {466const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel);467468let lastChangePartOfInlineEdit = false;469let inlineEditModelVersion = this._inlineEditModelVersion;470let newAction: InlineSuggestionAction | undefined;471472if (this.action?.kind === 'edit') { // TODO What about rename?473edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges));474475if (edits.some(edit => edit.edit === undefined)) {476return undefined; // change is invalid, so we will have to drop the completion477}478479480const newTextModelVersion = textModel.getVersionId();481lastChangePartOfInlineEdit = edits.some(edit => edit.lastChangeUpdatedEdit);482if (lastChangePartOfInlineEdit) {483inlineEditModelVersion = newTextModelVersion ?? -1;484}485486if (newTextModelVersion === null || inlineEditModelVersion + 20 < newTextModelVersion) {487return undefined; // the completion has been ignored for a while, remove it488}489490edits = edits.filter(innerEdit => !innerEdit.edit!.isEmpty);491if (edits.length === 0) {492return undefined; // the completion has been typed by the user493}494495const newEdit = new StringEdit(edits.map(edit => edit.edit!));496497const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toReplacement(new TextModelText(textModel));498499newAction = {500kind: 'edit',501textReplacement: newTextEdit,502snippetInfo: this.snippetInfo,503stringEdit: newEdit,504uri: this.action.uri,505alternativeAction: this.action.alternativeAction,506};507} else if (this.action?.kind === 'jumpTo') {508const jumpToOffset = this.action.offset;509const newJumpToOffset = textModelChanges.applyToOffsetOrUndefined(jumpToOffset);510if (newJumpToOffset === undefined) {511return undefined;512}513const newJumpToPosition = positionOffsetTransformer.getPosition(newJumpToOffset);514515newAction = {516kind: 'jumpTo',517position: newJumpToPosition,518offset: newJumpToOffset,519uri: this.action.uri,520};521} else {522newAction = undefined;523}524525let newDisplayLocation = this.hint;526if (newDisplayLocation) {527newDisplayLocation = newDisplayLocation.withEdit(textModelChanges, positionOffsetTransformer);528if (!newDisplayLocation) {529return undefined;530}531}532533return new InlineEditItem(534newAction,535this._data,536this.identity,537edits,538newDisplayLocation,539lastChangePartOfInlineEdit,540inlineEditModelVersion,541);542}543544override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined {545const edit = this.action?.kind === 'edit' ? this.action.stringEdit : undefined;546if (!edit) {547return undefined;548}549return computeEditKind(edit, model);550}551}552553function getDiffedStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit {554const eol = textModel.getEOL();555const editOriginalText = textModel.getValueInRange(editRange);556const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol);557558const diffAlgorithm = linesDiffComputers.getDefault();559const lineDiffs = diffAlgorithm.computeDiff(560splitLines(editOriginalText),561splitLines(editReplaceText),562{563ignoreTrimWhitespace: false,564computeMoves: false,565extendToSubwords: true,566maxComputationTimeMs: 50,567}568);569570const innerChanges = lineDiffs.changes.flatMap(c => c.innerChanges ?? []);571572function addRangeToPos(pos: Position, range: Range): Range {573const start = TextLength.fromPosition(range.getStartPosition());574return TextLength.ofRange(range).createRange(start.addToPosition(pos));575}576577const modifiedText = new StringText(editReplaceText);578579const offsetEdit = new StringEdit(580innerChanges.map(c => {581const rangeInModel = addRangeToPos(editRange.getStartPosition(), c.originalRange);582const originalRange = getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(rangeInModel);583584const replaceText = modifiedText.getValueOfRange(c.modifiedRange);585const edit = new StringReplacement(originalRange, replaceText);586587const originalText = textModel.getValueInRange(rangeInModel);588return reshapeInlineEdit(edit, originalText, innerChanges.length, textModel);589})590);591592return offsetEdit;593}594595function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit {596return new StringEdit([new StringReplacement(597getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(editRange),598replaceText599)]);600}601602class SingleUpdatedNextEdit {603public static create(604edit: StringReplacement,605replacedText: string,606): SingleUpdatedNextEdit {607const prefixLength = commonPrefixLength(edit.newText, replacedText);608const suffixLength = commonSuffixLength(edit.newText, replacedText);609const trimmedNewText = edit.newText.substring(prefixLength, edit.newText.length - suffixLength);610return new SingleUpdatedNextEdit(edit, trimmedNewText, prefixLength, suffixLength);611}612613public get edit() { return this._edit; }614public get lastChangeUpdatedEdit() { return this._lastChangeUpdatedEdit; }615616constructor(617private _edit: StringReplacement | undefined,618private _trimmedNewText: string,619private _prefixLength: number,620private _suffixLength: number,621private _lastChangeUpdatedEdit: boolean = false,622) {623}624625public applyTextModelChanges(textModelChanges: StringEdit) {626const c = this._clone();627c._applyTextModelChanges(textModelChanges);628return c;629}630631private _clone(): SingleUpdatedNextEdit {632return new SingleUpdatedNextEdit(633this._edit,634this._trimmedNewText,635this._prefixLength,636this._suffixLength,637this._lastChangeUpdatedEdit,638);639}640641private _applyTextModelChanges(textModelChanges: StringEdit) {642this._lastChangeUpdatedEdit = false; // TODO @benibenj make immutable643644if (!this._edit) {645throw new BugIndicatingError('UpdatedInnerEdits: No edit to apply changes to');646}647648const result = this._applyChanges(this._edit, textModelChanges);649if (!result) {650this._edit = undefined;651return;652}653654this._edit = result.edit;655this._lastChangeUpdatedEdit = result.editHasChanged;656}657658private _applyChanges(edit: StringReplacement, textModelChanges: StringEdit): { edit: StringReplacement; editHasChanged: boolean } | undefined {659let editStart = edit.replaceRange.start;660let editEnd = edit.replaceRange.endExclusive;661let editReplaceText = edit.newText;662let editHasChanged = false;663664const shouldPreserveEditShape = this._prefixLength > 0 || this._suffixLength > 0;665666for (let i = textModelChanges.replacements.length - 1; i >= 0; i--) {667const change = textModelChanges.replacements[i];668669// INSERTIONS (only support inserting at start of edit)670const isInsertion = change.newText.length > 0 && change.replaceRange.isEmpty;671672if (isInsertion && !shouldPreserveEditShape && change.replaceRange.start === editStart && editReplaceText.startsWith(change.newText)) {673editStart += change.newText.length;674editReplaceText = editReplaceText.substring(change.newText.length);675editEnd += change.newText.length;676editHasChanged = true;677continue;678}679680if (isInsertion && shouldPreserveEditShape && change.replaceRange.start === editStart + this._prefixLength && this._trimmedNewText.startsWith(change.newText)) {681editEnd += change.newText.length;682editHasChanged = true;683this._prefixLength += change.newText.length;684this._trimmedNewText = this._trimmedNewText.substring(change.newText.length);685continue;686}687688// DELETIONS689const isDeletion = change.newText.length === 0 && change.replaceRange.length > 0;690if (isDeletion && change.replaceRange.start >= editStart + this._prefixLength && change.replaceRange.endExclusive <= editEnd - this._suffixLength) {691// user deleted text IN-BETWEEN the deletion range692editEnd -= change.replaceRange.length;693editHasChanged = true;694continue;695}696697// user did exactly the edit698if (change.equals(edit)) {699editHasChanged = true;700editStart = change.replaceRange.endExclusive;701editReplaceText = '';702continue;703}704705// MOVE EDIT706if (change.replaceRange.start > editEnd) {707// the change happens after the completion range708continue;709}710if (change.replaceRange.endExclusive < editStart) {711// the change happens before the completion range712editStart += change.newText.length - change.replaceRange.length;713editEnd += change.newText.length - change.replaceRange.length;714continue;715}716717// The change intersects the completion, so we will have to drop the completion718return undefined;719}720721// the resulting edit is a noop as the original and new text are the same722if (this._trimmedNewText.length === 0 && editStart + this._prefixLength === editEnd - this._suffixLength) {723return { edit: new StringReplacement(new OffsetRange(editStart + this._prefixLength, editStart + this._prefixLength), ''), editHasChanged: true };724}725726return { edit: new StringReplacement(new OffsetRange(editStart, editEnd), editReplaceText), editHasChanged };727}728}729730function reshapeInlineCompletion(edit: StringReplacement, textModel: ITextModel): StringReplacement {731// If the insertion is a multi line insertion starting on the next line732// Move it forwards so that the multi line insertion starts on the current line733const eol = textModel.getEOL();734if (edit.replaceRange.isEmpty && edit.newText.includes(eol)) {735edit = reshapeMultiLineInsertion(edit, textModel);736}737738return edit;739}740741function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: ITextModel): StringReplacement {742// TODO: EOL are not properly trimmed by the diffAlgorithm #12680743const eol = textModel.getEOL();744if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) {745edit = new StringReplacement(edit.replaceRange.deltaEnd(-eol.length), edit.newText.slice(0, -eol.length));746}747748// INSERTION749// If the insertion ends with a new line and is inserted at the start of a line which has text,750// we move the insertion to the end of the previous line if possible751if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) {752const startPosition = textModel.getPositionAt(edit.replaceRange.start);753const hasTextOnInsertionLine = textModel.getLineLength(startPosition.lineNumber) !== 0;754if (hasTextOnInsertionLine) {755edit = reshapeMultiLineInsertion(edit, textModel);756}757}758759// The diff algorithm extended a simple edit to the entire word760// shrink it back to a simple edit if it is deletion/insertion only761if (totalInnerEdits === 1) {762const prefixLength = commonPrefixLength(originalText, edit.newText);763const suffixLength = commonSuffixLength(originalText.slice(prefixLength), edit.newText.slice(prefixLength));764765// reshape it back to an insertion766if (prefixLength + suffixLength === originalText.length) {767return new StringReplacement(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), edit.newText.substring(prefixLength, edit.newText.length - suffixLength));768}769770// reshape it back to a deletion771if (prefixLength + suffixLength === edit.newText.length) {772return new StringReplacement(edit.replaceRange.deltaStart(prefixLength).deltaEnd(-suffixLength), '');773}774}775776return edit;777}778779function reshapeMultiLineInsertion(edit: StringReplacement, textModel: ITextModel): StringReplacement {780if (!edit.replaceRange.isEmpty) {781throw new BugIndicatingError('Unexpected original range');782}783784if (edit.replaceRange.start === 0) {785return edit;786}787788const eol = textModel.getEOL();789const startPosition = textModel.getPositionAt(edit.replaceRange.start);790const startColumn = startPosition.column;791const startLineNumber = startPosition.lineNumber;792793// 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 (startColumn === 1 && startLineNumber > 1 && edit.newText.endsWith(eol) && !edit.newText.startsWith(eol)) {796return new StringReplacement(edit.replaceRange.delta(-1), eol + edit.newText.slice(0, -eol.length));797}798799return edit;800}801802803