mirror of https://github.com/microsoft/vscode.git
Adds disposeInlineCompletions reason (#251614)
* Ads disposeInlineCompletions reason
This commit is contained in:
parent
5272595668
commit
846585a0e0
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() { }
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue