Fixes flickering (#252310)

This commit is contained in:
Henning Dieterichs
2025-06-24 20:01:33 +02:00
committed by GitHub
parent 21e7d58551
commit 848883ca9c
3 changed files with 75 additions and 21 deletions
@@ -376,16 +376,18 @@ class InlineCompletionsState extends Disposable {
return new InlineCompletionsState(newInlineCompletions, this.request);
}
public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserve: InlineSuggestionIdentity | undefined): InlineCompletionsState {
let updatedItems: InlineSuggestionItem[] = [];
public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserveAtTop: InlineSuggestionIdentity | undefined): InlineCompletionsState {
let itemToPreserve: InlineSuggestionItem | undefined = undefined;
if (itemIdToPreserve) {
const preserveCandidate = this._findById(itemIdToPreserve);
if (preserveCandidate) {
const updatedSuggestionsHasItemToPreserve = updatedSuggestions.some(i => i.hash === preserveCandidate.hash);
if (!updatedSuggestionsHasItemToPreserve && preserveCandidate.canBeReused(textModel, request.position)) {
itemToPreserve = preserveCandidate;
if (itemIdToPreserveAtTop) {
const itemToPreserveCandidate = this._findById(itemIdToPreserveAtTop);
if (itemToPreserveCandidate && itemToPreserveCandidate.canBeReused(textModel, request.position)) {
itemToPreserve = itemToPreserveCandidate;
const updatedItemToPreserve = updatedSuggestions.find(i => i.hash === itemToPreserveCandidate.hash);
if (updatedItemToPreserve) {
updatedSuggestions = moveToFront(updatedItemToPreserve, updatedSuggestions);
} else {
updatedSuggestions = [itemToPreserveCandidate, ...updatedSuggestions];
}
}
}
@@ -396,22 +398,20 @@ class InlineCompletionsState extends Disposable {
// Otherwise: prefer inline completion if there is a visible one
: updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition));
const updatedItems: InlineSuggestionItem[] = [];
for (const i of updatedSuggestions) {
const oldItem = this._findByHash(i.hash);
if (oldItem) {
updatedItems.push(i.withIdentity(oldItem.identity));
let item;
if (oldItem && oldItem !== i) {
item = i.withIdentity(oldItem.identity);
oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() });
} else {
updatedItems.push(i);
item = i;
}
if (preferInlineCompletions !== item.isInlineEdit) {
updatedItems.push(item);
}
}
if (itemToPreserve) {
updatedItems.unshift(itemToPreserve);
}
updatedItems = preferInlineCompletions ? updatedItems.filter(i => !i.isInlineEdit) : updatedItems.filter(i => i.isInlineEdit);
return new InlineCompletionsState(updatedItems, request);
}
@@ -419,3 +419,11 @@ class InlineCompletionsState extends Disposable {
return new InlineCompletionsState(this.inlineCompletions, this.request);
}
}
function moveToFront<T>(item: T, items: T[]): T[] {
const index = items.indexOf(item);
if (index > -1) {
return [item, ...items.slice(0, index), ...items.slice(index + 1)];
}
return items;
}
@@ -420,6 +420,48 @@ suite('Inline Completions', () => {
}
);
});
test('Push item to preserve to front', async function () {
const provider = new MockInlineCompletionsProvider(true);
await withAsyncTestCodeEditorAndInlineCompletionsModel('',
{ fakeClock: true, provider },
async ({ editor, editorViewModel, model, context }) => {
provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) });
context.keyboardType('foo');
await timeout(1000);
assert.deepStrictEqual(provider.getAndClearCallHistory(), ([
{
position: "(1,4)",
triggerKind: 0,
text: "foo"
}
]));
assert.deepStrictEqual(context.getAndClearViewStates(),
([
"",
"foo[bar]"
])
);
provider.setReturnValues([{ insertText: 'foobar1', range: new Range(1, 1, 1, 4) }, { insertText: 'foobar', range: new Range(1, 1, 1, 4) }]);
await model.triggerExplicitly();
await timeout(1000);
assert.deepStrictEqual(provider.getAndClearCallHistory(), ([
{
position: "(1,4)",
triggerKind: 1,
text: "foo"
}
]));
assert.deepStrictEqual(context.getAndClearViewStates(),
([])
);
}
);
});
});
test('No race conditions', async function () {
@@ -32,6 +32,10 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider
private callHistory = new Array<unknown>();
private calledTwiceIn50Ms = false;
constructor(
public readonly enableForwardStability = false,
) { }
public setReturnValue(value: InlineCompletion | undefined, delayMs: number = 0): void {
this.returnValue = value ? [value] : [];
this.delayMs = delayMs;
@@ -56,7 +60,7 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider
private lastTimeMs: number | undefined = undefined;
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken) {
async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletions> {
const currentTimeMs = new Date().getTime();
if (this.lastTimeMs && currentTimeMs - this.lastTimeMs < 50) {
this.calledTwiceIn50Ms = true;
@@ -81,7 +85,7 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider
await timeout(this.delayMs);
}
return { items: result };
return { items: result, enableForwardStability: this.enableForwardStability };
}
disposeInlineCompletions() { }
handleItemDidShow() { }