Adds disposeInlineCompletions reason (#251614)

* Ads disposeInlineCompletions reason
This commit is contained in:
Henning Dieterichs 2025-06-17 15:36:54 +02:00 committed by GitHub
parent 5272595668
commit 846585a0e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 68 additions and 67 deletions

View File

@ -901,7 +901,7 @@ export interface InlineCompletionsProvider<T extends InlineCompletions = InlineC
/**
* Will be called when a completions list is no longer in use and can be garbage-collected.
*/
freeInlineCompletions(completions: T): void;
disposeInlineCompletions(completions: T, reason: InlineCompletionsDisposeReason): void;
onDidChangeInlineCompletions?: Event<void>;
@ -924,6 +924,8 @@ export interface InlineCompletionsProvider<T extends InlineCompletions = InlineC
toString?(): string;
}
export type InlineCompletionsDisposeReason = 'lostRace' | 'tokenCancellation' | 'other';
export enum InlineCompletionEndOfLifeReasonKind {
Accepted = 0,
Rejected = 1,

View File

@ -923,7 +923,7 @@ export class InlineCompletionsModel extends Disposable {
const cursorPosition = positions[0];
// Executing the edit might free the completion, so we have to hold a reference on it.
completion.source.addRef();
completion.addRef();
try {
this._isAcceptingPartially = true;
try {
@ -949,7 +949,7 @@ export class InlineCompletionsModel extends Disposable {
completion.reportPartialAccept(acceptedLength, { kind, acceptedLength: acceptedLength });
} finally {
completion.source.removeRef();
completion.removeRef();
}
}

View File

@ -16,7 +16,7 @@ import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
import { Position } from '../../../../common/core/position.js';
import { Range } from '../../../../common/core/range.js';
import { TextReplacement } from '../../../../common/core/edits/textEdit.js';
import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind, PartialAcceptInfo } from '../../../../common/languages.js';
import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind, PartialAcceptInfo, InlineCompletionsDisposeReason } from '../../../../common/languages.js';
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
import { ITextModel } from '../../../../common/model.js';
import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js';
@ -37,9 +37,19 @@ export async function provideInlineCompletions(
baseToken: CancellationToken = CancellationToken.None,
languageConfigurationService?: ILanguageConfigurationService,
): Promise<InlineCompletionProviderResult> {
if (baseToken.isCancellationRequested) {
return new InlineCompletionProviderResult([], new Set(), []);
}
const requestUuid = generateUuid();
const tokenSource = new CancellationTokenSource(baseToken);
const token = tokenSource.token;
const lostRaceTokenSource = new CancellationTokenSource();
const lostRaceToken = lostRaceTokenSource.token;
const combinedTokenSource = new CancellationTokenSource(baseToken);
runWhenCancelled(lostRaceToken, () => combinedTokenSource.dispose(true));
const combinedToken = combinedTokenSource.token;
const contextWithUuid: InlineCompletionContext = { ...context, requestUuid: requestUuid };
const defaultReplaceRange = positionOrRange instanceof Position ? getDefaultRange(positionOrRange, model) : positionOrRange;
@ -54,11 +64,15 @@ export async function provideInlineCompletions(
+ ` Path: ${foundCycles.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`));
}
const queryProviderOrPreferredProvider = new CachedFunction(async (provider: InlineCompletionsProvider<InlineCompletions>): Promise<InlineSuggestionList | undefined> => {
const queryProvider = new CachedFunction(async (provider: InlineCompletionsProvider<InlineCompletions>): Promise<InlineSuggestionList | undefined> => {
if (combinedToken.isCancellationRequested) {
return undefined;
}
const yieldsTo = yieldsToGraph.getOutgoing(provider);
for (const p of yieldsTo) {
// We know there is no cycle, so no recursion here
const result = await queryProviderOrPreferredProvider.get(p);
const result = await queryProvider.get(p);
if (result && result.inlineSuggestions.items.length > 0) {
// Skip provider
return undefined;
@ -68,9 +82,9 @@ export async function provideInlineCompletions(
let result: InlineCompletions | null | undefined;
try {
if (positionOrRange instanceof Position) {
result = await provider.provideInlineCompletions(model, positionOrRange, contextWithUuid, token);
result = await provider.provideInlineCompletions(model, positionOrRange, contextWithUuid, combinedToken);
} else {
result = await provider.provideInlineEditsForRange?.(model, positionOrRange, contextWithUuid, token);
result = await provider.provideInlineEditsForRange?.(model, positionOrRange, contextWithUuid, combinedToken);
}
} catch (e) {
onUnexpectedExternalError(e);
@ -81,7 +95,9 @@ export async function provideInlineCompletions(
}
const data: InlineSuggestData[] = [];
const list = new InlineSuggestionList(result, data, provider);
runWhenCancelled(token, () => list.removeRef());
runWhenCancelled(combinedToken, () => {
return list.removeRef(lostRaceToken.isCancellationRequested ? 'lostRace' : 'tokenCancellation');
});
for (const item of result.items) {
data.push(createInlineCompletionItem(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid));
@ -90,59 +106,28 @@ export async function provideInlineCompletions(
return list;
});
const inlineCompletionLists = AsyncIterableObject.fromPromisesResolveOrder(providers.map(p => queryProviderOrPreferredProvider.get(p)));
if (token.isCancellationRequested) {
tokenSource.dispose(true);
// result has been disposed before we could call addRef! So we have to discard everything.
return new InlineCompletionProviderResult([], new Set(), []);
}
const result = await addRefAndCreateResult(contextWithUuid, inlineCompletionLists, model);
tokenSource.dispose(true); // This disposes results that are not referenced by now.
return result;
}
/** If the token does not leak, this will not leak either. */
function runWhenCancelled(token: CancellationToken, callback: () => void): IDisposable {
if (token.isCancellationRequested) {
callback();
return Disposable.None;
} else {
const listener = token.onCancellationRequested(() => {
listener.dispose();
callback();
});
return { dispose: () => listener.dispose() };
}
}
async function addRefAndCreateResult(
context: InlineCompletionContext,
inlineCompletionLists: AsyncIterable<(InlineSuggestionList | undefined)>,
model: ITextModel,
): Promise<InlineCompletionProviderResult> {
// for deduplication
const itemsByHash = new Map<string, InlineSuggestData>();
const inlineCompletionLists = AsyncIterableObject.fromPromisesResolveOrder(providers.map(p => queryProvider.get(p)));
const itemsByHash = new Map<string, InlineSuggestData>(); // for deduplication
let shouldStop = false;
const lists: InlineSuggestionList[] = [];
for await (const completions of inlineCompletionLists) {
if (!completions) { continue; }
if (!completions) {
continue;
}
completions.addRef();
lists.push(completions);
for (const item of completions.inlineSuggestionsData) {
if (!context.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) {
if (!contextWithUuid.includeInlineEdits && (item.isInlineEdit || item.showInlineEditMenu)) {
continue;
}
if (!context.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) {
if (!contextWithUuid.includeInlineCompletions && !(item.isInlineEdit || item.showInlineEditMenu)) {
continue;
}
itemsByHash.set(createHashFromSingleTextEdit(item.getSingleTextEdit()), item);
// Stop after first visible inline completion
if (!(item.isInlineEdit || item.showInlineEditMenu) && context.triggerKind === InlineCompletionTriggerKind.Automatic) {
if (!(item.isInlineEdit || item.showInlineEditMenu) && contextWithUuid.triggerKind === InlineCompletionTriggerKind.Automatic) {
const minifiedEdit = item.getSingleTextEdit().removeCommonPrefix(new TextModelText(model));
if (!minifiedEdit.isEmpty) {
shouldStop = true;
@ -155,7 +140,25 @@ async function addRefAndCreateResult(
}
}
return new InlineCompletionProviderResult(Array.from(itemsByHash.values()), new Set(itemsByHash.keys()), lists);
const result = new InlineCompletionProviderResult(Array.from(itemsByHash.values()), new Set(itemsByHash.keys()), lists);
lostRaceTokenSource.dispose(true); // This disposes results that are not referenced by now.
combinedTokenSource.dispose(true);
return result;
}
/** If the token is eventually cancelled, this will not leak either. */
function runWhenCancelled(token: CancellationToken, callback: () => void): IDisposable {
if (token.isCancellationRequested) {
callback();
return Disposable.None;
} else {
const listener = token.onCancellationRequested(() => {
listener.dispose();
callback();
});
return { dispose: () => listener.dispose() };
}
}
export class InlineCompletionProviderResult implements IDisposable {
@ -378,14 +381,14 @@ export class InlineSuggestionList {
this.refCount++;
}
removeRef(): void {
removeRef(reason: InlineCompletionsDisposeReason = 'other'): void {
this.refCount--;
if (this.refCount === 0) {
for (const item of this.inlineSuggestionsData) {
// Fallback if it has not been called before
item.reportEndOfLife();
}
this.provider.freeInlineCompletions(this.inlineSuggestions);
this.provider.disposeInlineCompletions(this.inlineSuggestions, reason);
}
}
}

View File

@ -83,7 +83,7 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider
return { items: result };
}
freeInlineCompletions() { }
disposeInlineCompletions() { }
handleItemDidShow() { }
}
@ -110,7 +110,7 @@ export class MockSearchReplaceCompletionsProvider implements InlineCompletionsPr
}
return { items: [] };
}
freeInlineCompletions() { }
disposeInlineCompletions() { }
handleItemDidShow() { }
}

View File

@ -222,7 +222,7 @@ export class SuggestInlineCompletions extends Disposable implements InlineComple
item.completion.resolve(CancellationToken.None);
}
freeInlineCompletions(result: InlineCompletionResults): void {
disposeInlineCompletions(result: InlineCompletionResults): void {
result.release();
}

View File

@ -85,7 +85,7 @@ suite('Suggest Inline Completions', function () {
// (1,3), end of word -> suggestions
const result = await completions.provideInlineCompletions(model, new Position(1, 3), context, CancellationToken.None);
assert.strictEqual(result?.items.length, 3);
completions.freeInlineCompletions(result);
completions.disposeInlineCompletions(result);
}
{
// (1,2), middle of word -> NO suggestions
@ -101,7 +101,7 @@ suite('Suggest Inline Completions', function () {
// unfiltered
const result = await completions.provideInlineCompletions(model, new Position(1, 3), context, CancellationToken.None);
assert.strictEqual(result?.items.length, 3);
completions.freeInlineCompletions(result);
completions.disposeInlineCompletions(result);
}
{
@ -109,7 +109,7 @@ suite('Suggest Inline Completions', function () {
editor.updateOptions({ suggest: { showSnippets: false } });
const result = await completions.provideInlineCompletions(model, new Position(1, 3), context, CancellationToken.None);
assert.strictEqual(result?.items.length, 2);
completions.freeInlineCompletions(result);
completions.disposeInlineCompletions(result);
}
});

4
src/vs/monaco.d.ts vendored
View File

@ -7426,7 +7426,7 @@ declare namespace monaco.languages {
/**
* Will be called when a completions list is no longer in use and can be garbage-collected.
*/
freeInlineCompletions(completions: T): void;
disposeInlineCompletions(completions: T, reason: InlineCompletionsDisposeReason): void;
onDidChangeInlineCompletions?: IEvent<void>;
/**
* Only used for {@link yieldsToGroupIds}.
@ -7443,6 +7443,8 @@ declare namespace monaco.languages {
toString?(): string;
}
export type InlineCompletionsDisposeReason = 'lostRace' | 'tokenCancellation' | 'other';
export enum InlineCompletionEndOfLifeReasonKind {
Accepted = 0,
Rejected = 1,

View File

@ -648,7 +648,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
await this._proxy.$handleInlineCompletionEndOfLifetime(handle, completions.pid, item.idx, mapReason(reason, i => ({ pid: completions.pid, idx: i.idx })));
}
},
freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => {
disposeInlineCompletions: (completions: IdentifiableInlineCompletions): void => {
this._proxy.$freeInlineCompletionsList(handle, completions.pid);
},
handleRejection: async (completions, item): Promise<void> => {

View File

@ -1381,12 +1381,6 @@ class InlineCompletionAdapter {
return undefined;
}
if (token.isCancellationRequested) {
// cancelled -> return without further ado, esp no caching
// of results as they will leak
return undefined;
}
const normalizedResult = Array.isArray(result) ? result : result.items;
const commands = this._isAdditionsProposedApiEnabled ? Array.isArray(result) ? [] : result.commands || [] : [];
const enableForwardStability = this._isAdditionsProposedApiEnabled && !Array.isArray(result) ? result.enableForwardStability : undefined;