Skip to content

Commit

Permalink
Support forwardStable for inline edits
Browse files Browse the repository at this point in the history
  • Loading branch information
benibenj committed Jan 21, 2025
1 parent 3f5e781 commit de10bcf
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class InlineCompletionsModel extends Disposable {
includeInlineCompletions: !changeSummary.onlyRequestInlineEdits,
includeInlineEdits: this._inlineEditsEnabled.read(reader),
};
const itemToPreserveCandidate = this.selectedInlineCompletion.get();
const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit;
const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable
? itemToPreserveCandidate : undefined;
return this._source.fetch(cursorPosition, context, itemToPreserve);
Expand Down Expand Up @@ -376,27 +376,32 @@ export class InlineCompletionsModel extends Disposable {
const model = this.textModel;

const item = this._inlineCompletionItems.read(reader);
if (item?.inlineEdit) {
let edit = item.inlineEdit.toSingleTextEdit(reader);
const inlineEditResult = item?.inlineEdit;
if (inlineEditResult) {
if (inlineEditResult.inlineEdit.read(reader) === null) {
return undefined;
}

let edit = inlineEditResult.toSingleTextEdit(reader);
edit = singleTextRemoveCommonPrefix(edit, model);

const cursorPos = this.primaryPosition.read(reader);
const cursorAtInlineEdit = LineRange.fromRangeInclusive(edit.range).addMargin(1, 1).contains(cursorPos.lineNumber);
const cursorInsideShowRange = cursorAtInlineEdit || (item.inlineEdit.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true);
const cursorInsideShowRange = cursorAtInlineEdit || (inlineEditResult.inlineCompletion.cursorShowRange?.containsPosition(cursorPos) ?? true);

if (!cursorInsideShowRange && !this._inAcceptFlow.read(reader)) {
return undefined;
}

const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this.primaryPosition.read(reader).lineNumber);
const disableCollapsing = true;
const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEdit.semanticId);
const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === inlineEditResult.semanticId);

const commands = item.inlineEdit.inlineCompletion.source.inlineCompletions.commands;
const commands = inlineEditResult.inlineCompletion.source.inlineCompletions.commands;
const renderExplicitly = this._jumpedTo.read(reader);
const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], item.inlineEdit.inlineCompletion);
const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? [], inlineEditResult.inlineCompletion);

return { kind: 'inlineEdit', inlineEdit, inlineCompletion: item.inlineEdit, edits: [edit], cursorAtInlineEdit };
return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: [edit], cursorAtInlineEdit };
}

this._jumpedTo.set(false, undefined);
Expand Down Expand Up @@ -590,7 +595,7 @@ export class InlineCompletionsModel extends Disposable {
completion = state.inlineCompletion.toInlineCompletion(undefined);
} else if (state?.kind === 'inlineEdit') {
completion = state.inlineCompletion.toInlineCompletion(undefined);
acceptDecorationOffsetRanges.push(...(state.inlineCompletion.inlineEdit?.getNewTextRanges() ?? []));
acceptDecorationOffsetRanges.push(...(state.inlineCompletion.inlineEdit?.get()?.getNewTextRanges() ?? []));
} else {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export class InlineCompletionsSource extends Disposable {
public readonly suggestWidgetInlineCompletions = disposableObservableValue<UpToDateInlineCompletions | undefined>('suggestWidgetInlineCompletions', undefined);

private readonly _loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store);
private readonly _invalidationDelay = observableConfigValue<number>('editor.inlineSuggest.edits.experimental.invalidationDelay', 4000, this._configurationService).recomputeInitiallyAndOnChange(this._store);

private readonly _structuredFetchLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast<
{ kind: 'start'; requestId: number; context: unknown } & IRecordableEditorLogEntry
| { kind: 'end'; error: any; durationMs: number; result: unknown; requestId: number } & IRecordableLogEntry
Expand Down Expand Up @@ -153,7 +155,7 @@ export class InlineCompletionsSource extends Disposable {
const endTime = new Date();
this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime());

const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId);
const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId, this._invalidationDelay);
if (activeInlineCompletion) {
const asInlineCompletion = activeInlineCompletion.toInlineCompletion(undefined);
if (activeInlineCompletion.canBeReused(this._textModel, position) && !updatedCompletions.has(asInlineCompletion)) {
Expand Down Expand Up @@ -258,6 +260,7 @@ export class UpToDateInlineCompletions implements IDisposable {
public readonly request: UpdateRequest,
private readonly _textModel: ITextModel,
private readonly _versionId: IObservable<number | null>,
private readonly _invalidationDelay: IObservable<number>,
) {
const ids = _textModel.deltaDecorations([], inlineCompletionProviderResult.completions.map(i => ({
range: i.range,
Expand All @@ -267,7 +270,7 @@ export class UpToDateInlineCompletions implements IDisposable {
})));

this._inlineCompletions = inlineCompletionProviderResult.completions.map(
(i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId, this.request)
(i, index) => new InlineCompletionWithUpdatedRange(i, ids[index], this._textModel, this._versionId, this._invalidationDelay, this.request)
);
}

Expand Down Expand Up @@ -310,7 +313,7 @@ export class UpToDateInlineCompletions implements IDisposable {
description: 'inline-completion-tracking-range'
},
}])[0];
this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId, this.request));
this._inlineCompletions.unshift(new InlineCompletionWithUpdatedRange(inlineCompletion, id, this._textModel, this._versionId, this._invalidationDelay, this.request));
this._prependedInlineCompletionItems.push(inlineCompletion);
}
}
Expand Down Expand Up @@ -339,19 +342,22 @@ export class InlineCompletionWithUpdatedRange {
/**
* This will be null for ghost text completions
*/
public _inlineEdit: ISettableObservable<OffsetEdit | null>;
public get inlineEdit() { return this._inlineEdit.get(); }
private _inlineEdit: ISettableObservable<OffsetEdit | null>;
public get inlineEdit(): IObservable<OffsetEdit | null> { return this._inlineEdit; }

public get source() { return this.inlineCompletion.source; }
public get sourceInlineCompletion() { return this.inlineCompletion.sourceInlineCompletion; }

private readonly _creationTime: number = Date.now();
private _invalidationTime: number | undefined = Date.now() + this._invalidationDelay.get();

private _lastChangePartOfInlineEdit = false;

constructor(
public readonly inlineCompletion: InlineCompletionItem,
public readonly decorationId: string,
private readonly _textModel: ITextModel,
private readonly _modelVersion: IObservable<number | null>,
private readonly _invalidationDelay: IObservable<number>,
public readonly request: UpdateRequest,
) {
const inlineCompletions = this.inlineCompletion.source.inlineCompletions.items;
Expand Down Expand Up @@ -382,33 +388,54 @@ export class InlineCompletionWithUpdatedRange {
}

public acceptTextModelChangeEvent(e: IModelContentChangedEvent, tx: ITransaction): void {
this._lastChangePartOfInlineEdit = false;

const offsetEdit = this._inlineEdit.get();
if (!offsetEdit) {
return;
}

if (this._creationTime + 4000 < Date.now()) {
const editUpdates = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes));

const emptyEdit = editUpdates.find(editUpdate => editUpdate.edit.isEmpty);
if (emptyEdit) {
// A change collided with one of our edits, so we will have to drop the completion
this._inlineEdit.set(new OffsetEdit([emptyEdit.edit]), tx);
return;
}

const partOfInlineEdit = editUpdates.some(({ changePartOfEdit }) => changePartOfEdit);
if (partOfInlineEdit) {
this._invalidationTime = undefined;
}

if (this._invalidationTime && this._invalidationTime < Date.now()) {
// The completion has been shown for a while and the user
// has been working on a different part of the document, so invalidate it
this._inlineEdit.set(new OffsetEdit([new SingleOffsetEdit(new OffsetRange(0, 0), '')]), tx);
return;
}

const newEdits = offsetEdit.edits.map(edit => acceptTextModelChange(edit, e.changes));
const emptyEdit = newEdits.find(edit => edit.isEmpty);
if (emptyEdit) {
// A change collided with one of our edits, so we will have to drop the completion
this._inlineEdit.set(new OffsetEdit([emptyEdit]), tx);
return;
}
this._inlineEdit.set(new OffsetEdit(newEdits), tx);
this._lastChangePartOfInlineEdit = partOfInlineEdit;
this._inlineEdit.set(new OffsetEdit(editUpdates.map(({ edit }) => edit)), tx);

function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): SingleOffsetEdit {
function acceptTextModelChange(edit: SingleOffsetEdit, changes: readonly IModelContentChange[]): { edit: SingleOffsetEdit; changePartOfEdit: boolean } {
let start = edit.replaceRange.start;
let end = edit.replaceRange.endExclusive;
let newText = edit.newText;
let changePartOfEdit = false;
for (let i = changes.length - 1; i >= 0; i--) {
const change = changes[i];

// user inserted text at the start of the completion
if (edit.replaceRange.isEmpty && change.rangeLength === 0 && change.rangeOffset === start && newText.startsWith(change.text)) {
start += change.text.length;
end = Math.max(start, end);
newText = newText.substring(change.text.length);
changePartOfEdit = true;
continue;
}

if (change.rangeOffset >= end) {
// the change happens after the completion range
continue;
Expand All @@ -425,7 +452,7 @@ export class InlineCompletionWithUpdatedRange {
end = change.rangeOffset;
newText = '';
}
return new SingleOffsetEdit(new OffsetRange(start, end), newText);
return { edit: new SingleOffsetEdit(new OffsetRange(start, end), newText), changePartOfEdit };
}
}

Expand Down Expand Up @@ -500,6 +527,13 @@ export class InlineCompletionWithUpdatedRange {
}

public canBeReused(model: ITextModel, position: Position): boolean {
const inlineEdit = this._inlineEdit.get();
if (inlineEdit !== null) {
return model === this._textModel
&& !inlineEdit.isEmpty
&& this._lastChangePartOfInlineEdit;
}

const updatedRange = this._updatedRange.read(undefined);
const result = !!updatedRange
&& updatedRange.containsPosition(position)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class InlineEditsAdapter extends Disposable {
};
}),
commands: definedEdits.flatMap(e => e.result.commands ?? []),
enableForwardStability: true,
};
},
handleRejection: (completions: InlineCompletions, item: InlineCompletionsAndEdits['items'][number]): void => {
Expand Down

0 comments on commit de10bcf

Please sign in to comment.