Skip to content

Commit d1ae8a6

Browse files
authored
Inline Completions: Implements yieldsTo (microsoft#187156)
Fixes #microsoft/vscode-internalbacklog#4055
1 parent e7776fd commit d1ae8a6

File tree

9 files changed

+157
-15
lines changed

9 files changed

+157
-15
lines changed

src/vs/base/common/collections.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,12 @@ export class SetMap<K, V> {
101101

102102
values.forEach(fn);
103103
}
104+
105+
get(key: K): ReadonlySet<V> {
106+
const values = this.map.get(key);
107+
if (!values) {
108+
return new Set<V>();
109+
}
110+
return new Set(values);
111+
}
104112
}

src/vs/editor/common/languages.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,8 @@ export interface InlineCompletions<TItem extends InlineCompletion = InlineComple
703703
readonly enableForwardStability?: boolean | undefined;
704704
}
705705

706+
export type InlineCompletionProviderGroupId = string;
707+
706708
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
707709
provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
708710

@@ -721,6 +723,20 @@ export interface InlineCompletionsProvider<T extends InlineCompletions = InlineC
721723
* Will be called when a completions list is no longer in use and can be garbage-collected.
722724
*/
723725
freeInlineCompletions(completions: T): void;
726+
727+
/**
728+
* Only used for {@link yieldsToGroupIds}.
729+
* Multiple providers can have the same group id.
730+
*/
731+
groupId?: InlineCompletionProviderGroupId;
732+
733+
/**
734+
* Returns a list of preferred provider {@link groupId}s.
735+
* The current provider is only requested for completions if no provider with a preferred group id returned a result.
736+
*/
737+
yieldsToGroupIds?: InlineCompletionProviderGroupId[];
738+
739+
toString?(): string;
724740
}
725741

726742
export interface CodeAction {

src/vs/editor/contrib/inlineCompletions/browser/provideInlineCompletions.ts

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { assertNever } from 'vs/base/common/assert';
7+
import { DeferredPromise } from 'vs/base/common/async';
78
import { CancellationToken } from 'vs/base/common/cancellation';
9+
import { SetMap } from 'vs/base/common/collections';
810
import { onUnexpectedExternalError } from 'vs/base/common/errors';
911
import { IDisposable } from 'vs/base/common/lifecycle';
1012
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
1113
import { Position } from 'vs/editor/common/core/position';
1214
import { Range } from 'vs/editor/common/core/range';
1315
import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
14-
import { Command, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from 'vs/editor/common/languages';
16+
import { Command, InlineCompletion, InlineCompletionContext, InlineCompletionProviderGroupId, InlineCompletions, InlineCompletionsProvider } from 'vs/editor/common/languages';
1517
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
1618
import { ITextModel } from 'vs/editor/common/model';
1719
import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets';
@@ -29,17 +31,87 @@ export async function provideInlineCompletions(
2931
): Promise<InlineCompletionProviderResult> {
3032
// Important: Don't use position after the await calls, as the model could have been changed in the meantime!
3133
const defaultReplaceRange = getDefaultRange(position, model);
32-
3334
const providers = registry.all(model);
34-
const providerResults = await Promise.all(providers.map(async provider => {
35+
36+
const multiMap = new SetMap<InlineCompletionProviderGroupId, InlineCompletionsProvider<any>>();
37+
for (const provider of providers) {
38+
if (provider.groupId) {
39+
multiMap.add(provider.groupId, provider);
40+
}
41+
}
42+
43+
function getPreferredProviders(provider: InlineCompletionsProvider<any>): InlineCompletionsProvider<any>[] {
44+
if (!provider.yieldsToGroupIds) { return []; }
45+
const result: InlineCompletionsProvider<any>[] = [];
46+
for (const groupId of provider.yieldsToGroupIds || []) {
47+
const providers = multiMap.get(groupId);
48+
for (const p of providers) {
49+
result.push(p);
50+
}
51+
}
52+
return result;
53+
}
54+
55+
type Result = Promise<InlineCompletions<InlineCompletion> | null | undefined>;
56+
const states = new Map<InlineCompletionsProvider<InlineCompletions<InlineCompletion>>, Result>();
57+
58+
const seen = new Set<InlineCompletionsProvider<InlineCompletions<InlineCompletion>>>();
59+
function findPreferredProviderCircle(provider: InlineCompletionsProvider<any>, stack: InlineCompletionsProvider[]): InlineCompletionsProvider[] | undefined {
60+
stack = [...stack, provider];
61+
if (seen.has(provider)) { return stack; }
62+
63+
seen.add(provider);
3564
try {
36-
const completions = await provider.provideInlineCompletions(model, position, context, token);
37-
return ({ provider, completions });
38-
} catch (e) {
39-
onUnexpectedExternalError(e);
65+
const preferred = getPreferredProviders(provider);
66+
for (const p of preferred) {
67+
const c = findPreferredProviderCircle(p, stack);
68+
if (c) { return c; }
69+
}
70+
} finally {
71+
seen.delete(provider);
4072
}
41-
return ({ provider, completions: undefined });
42-
}));
73+
return undefined;
74+
}
75+
76+
function processProvider(provider: InlineCompletionsProvider<any>): Result {
77+
const state = states.get(provider);
78+
if (state) {
79+
return state;
80+
}
81+
82+
const circle = findPreferredProviderCircle(provider, []);
83+
if (circle) {
84+
onUnexpectedExternalError(new Error(`Inline completions: cyclic yield-to dependency detected. Path: ${circle.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`));
85+
}
86+
87+
const deferredPromise = new DeferredPromise<InlineCompletions<InlineCompletion> | null | undefined>();
88+
states.set(provider, deferredPromise.p);
89+
90+
(async () => {
91+
if (!circle) {
92+
const preferred = getPreferredProviders(provider);
93+
for (const p of preferred) {
94+
const result = await processProvider(p);
95+
if (result && result.items.length > 0) {
96+
// Skip provider
97+
return undefined;
98+
}
99+
}
100+
}
101+
102+
try {
103+
const completions = await provider.provideInlineCompletions(model, position, context, token);
104+
return completions;
105+
} catch (e) {
106+
onUnexpectedExternalError(e);
107+
return undefined;
108+
}
109+
})().then(c => deferredPromise.complete(c), e => deferredPromise.error(e));
110+
111+
return deferredPromise.p;
112+
}
113+
114+
const providerResults = await Promise.all(providers.map(async provider => ({ provider, completions: await processProvider(provider) })));
43115

44116
const itemsByHash = new Map<string, InlineCompletionItem>();
45117
const lists: InlineCompletionList[] = [];

src/vs/monaco.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7090,6 +7090,8 @@ declare namespace monaco.languages {
70907090
readonly enableForwardStability?: boolean | undefined;
70917091
}
70927092

7093+
export type InlineCompletionProviderGroupId = string;
7094+
70937095
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
70947096
provideInlineCompletions(model: editor.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
70957097
/**
@@ -7105,6 +7107,17 @@ declare namespace monaco.languages {
71057107
* Will be called when a completions list is no longer in use and can be garbage-collected.
71067108
*/
71077109
freeInlineCompletions(completions: T): void;
7110+
/**
7111+
* Only used for {@link yieldsToGroupIds}.
7112+
* Multiple providers can have the same group id.
7113+
*/
7114+
groupId?: InlineCompletionProviderGroupId;
7115+
/**
7116+
* Returns a list of preferred provider {@link groupId}s.
7117+
* The current provider is only requested for completions if no provider with a preferred group id returned a result.
7118+
*/
7119+
yieldsToGroupIds?: InlineCompletionProviderGroupId[];
7120+
toString?(): string;
71087121
}
71097122

71107123
export interface CodeAction {

src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
557557
this._registrations.set(handle, this._languageFeaturesService.completionProvider.register(selector, provider));
558558
}
559559

560-
$registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean): void {
560+
$registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean, extensionId: string, yieldsToExtensionIds: string[]): void {
561561
const provider: languages.InlineCompletionsProvider<IdentifiableInlineCompletions> = {
562562
provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<IdentifiableInlineCompletions | undefined> => {
563563
return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token);
@@ -574,6 +574,11 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
574574
},
575575
freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => {
576576
this._proxy.$freeInlineCompletionsList(handle, completions.pid);
577+
},
578+
groupId: extensionId,
579+
yieldsToGroupIds: yieldsToExtensionIds,
580+
toString() {
581+
return `InlineCompletionsProvider(${extensionId})`;
577582
}
578583
};
579584
this._registrations.set(handle, this._languageFeaturesService.inlineCompletionsProvider.register(selector, provider));

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -578,14 +578,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
578578
registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable {
579579
return extHostLanguageFeatures.registerCompletionItemProvider(extension, checkSelector(selector), provider, triggerCharacters);
580580
},
581-
registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable {
581+
registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider, metadata?: vscode.InlineCompletionItemProviderMetadata): vscode.Disposable {
582582
if (provider.handleDidShowCompletionItem) {
583583
checkProposedApiEnabled(extension, 'inlineCompletionsAdditions');
584584
}
585585
if (provider.handleDidPartiallyAcceptCompletionItem) {
586586
checkProposedApiEnabled(extension, 'inlineCompletionsAdditions');
587587
}
588-
return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider);
588+
if (metadata) {
589+
checkProposedApiEnabled(extension, 'inlineCompletionsAdditions');
590+
}
591+
return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider, metadata);
589592
},
590593
registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable {
591594
return extHostLanguageFeatures.registerDocumentLinkProvider(extension, checkSelector(selector), provider);

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
407407
$emitDocumentSemanticTokensEvent(eventHandle: number): void;
408408
$registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void;
409409
$registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void;
410-
$registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean): void;
410+
$registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean, extensionId: string, yieldsToExtensionIds: string[]): void;
411411
$registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void;
412412
$registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void;
413413
$emitInlayHintsEvent(eventHandle: number): void;

src/vs/workbench/api/common/extHostLanguageFeatures.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,10 +2249,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
22492249

22502250
// --- ghost test
22512251

2252-
registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable {
2252+
registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider, metadata: vscode.InlineCompletionItemProviderMetadata | undefined): vscode.Disposable {
22532253
const adapter = new InlineCompletionAdapter(extension, this._documents, provider, this._commands.converter);
22542254
const handle = this._addNewAdapter(adapter, extension);
2255-
this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector, extension), adapter.supportsHandleEvents);
2255+
this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector, extension), adapter.supportsHandleEvents,
2256+
ExtensionIdentifier.toKey(extension.identifier.value), metadata?.yieldTo?.map(extId => ExtensionIdentifier.toKey(extId)) || []);
22562257
return this._createDisposable(handle);
22572258
}
22582259

src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ declare module 'vscode' {
77

88
// https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima
99

10+
export namespace languages {
11+
/**
12+
* Registers an inline completion provider.
13+
*
14+
* Multiple providers can be registered for a language. In that case providers are asked in
15+
* parallel and the results are merged. A failing provider (rejected promise or exception) will
16+
* not cause a failure of the whole operation.
17+
*
18+
* @param selector A selector that defines the documents this provider is applicable to.
19+
* @param provider An inline completion provider.
20+
* @param metadata Metadata about the provider.
21+
* @return A {@link Disposable} that unregisters this provider when being disposed.
22+
*/
23+
export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider, metadata: InlineCompletionItemProviderMetadata): Disposable;
24+
}
25+
1026
export interface InlineCompletionItem {
1127
/**
1228
* If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed.
@@ -15,6 +31,14 @@ declare module 'vscode' {
1531
completeBracketPairs?: boolean;
1632
}
1733

34+
export interface InlineCompletionItemProviderMetadata {
35+
/**
36+
* Specifies a list of extension ids that this provider yields to if they return a result.
37+
* If some inline completion provider registered by such an extension returns a result, this provider is not asked.
38+
*/
39+
yieldTo: string[];
40+
}
41+
1842
export interface InlineCompletionItemProvider {
1943
/**
2044
* @param completionItem The completion item that was shown.

0 commit comments

Comments
 (0)