diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index d8ee92f757e..95def40789d 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -101,4 +101,12 @@ export class SetMap { values.forEach(fn); } + + get(key: K): ReadonlySet { + const values = this.map.get(key); + if (!values) { + return new Set(); + } + return new Set(values); + } } diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 60a04c23d0a..364998995e3 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -703,6 +703,8 @@ export interface InlineCompletions { provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; @@ -721,6 +723,20 @@ export interface InlineCompletionsProvider { // Important: Don't use position after the await calls, as the model could have been changed in the meantime! const defaultReplaceRange = getDefaultRange(position, model); - const providers = registry.all(model); - const providerResults = await Promise.all(providers.map(async provider => { - try { - const completions = await provider.provideInlineCompletions(model, position, context, token); - return ({ provider, completions }); - } catch (e) { - onUnexpectedExternalError(e); + + const multiMap = new SetMap>(); + for (const provider of providers) { + if (provider.groupId) { + multiMap.add(provider.groupId, provider); } - return ({ provider, completions: undefined }); - })); + } + + function getPreferredProviders(provider: InlineCompletionsProvider): InlineCompletionsProvider[] { + if (!provider.yieldsToGroupIds) { return []; } + const result: InlineCompletionsProvider[] = []; + for (const groupId of provider.yieldsToGroupIds || []) { + const providers = multiMap.get(groupId); + for (const p of providers) { + result.push(p); + } + } + return result; + } + + type Result = Promise | null | undefined>; + const states = new Map>, Result>(); + + const seen = new Set>>(); + function findPreferredProviderCircle(provider: InlineCompletionsProvider, stack: InlineCompletionsProvider[]): InlineCompletionsProvider[] | undefined { + stack = [...stack, provider]; + if (seen.has(provider)) { return stack; } + + seen.add(provider); + try { + const preferred = getPreferredProviders(provider); + for (const p of preferred) { + const c = findPreferredProviderCircle(p, stack); + if (c) { return c; } + } + } finally { + seen.delete(provider); + } + return undefined; + } + + function processProvider(provider: InlineCompletionsProvider): Result { + const state = states.get(provider); + if (state) { + return state; + } + + const circle = findPreferredProviderCircle(provider, []); + if (circle) { + onUnexpectedExternalError(new Error(`Inline completions: cyclic yield-to dependency detected. Path: ${circle.map(s => s.toString ? s.toString() : ('' + s)).join(' -> ')}`)); + } + + const deferredPromise = new DeferredPromise | null | undefined>(); + states.set(provider, deferredPromise.p); + + (async () => { + if (!circle) { + const preferred = getPreferredProviders(provider); + for (const p of preferred) { + const result = await processProvider(p); + if (result && result.items.length > 0) { + // Skip provider + return undefined; + } + } + } + + try { + const completions = await provider.provideInlineCompletions(model, position, context, token); + return completions; + } catch (e) { + onUnexpectedExternalError(e); + return undefined; + } + })().then(c => deferredPromise.complete(c), e => deferredPromise.error(e)); + + return deferredPromise.p; + } + + const providerResults = await Promise.all(providers.map(async provider => ({ provider, completions: await processProvider(provider) }))); const itemsByHash = new Map(); const lists: InlineCompletionList[] = []; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 001b8a6de24..d59db506ee1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7090,6 +7090,8 @@ declare namespace monaco.languages { readonly enableForwardStability?: boolean | undefined; } + export type InlineCompletionProviderGroupId = string; + export interface InlineCompletionsProvider { provideInlineCompletions(model: editor.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; /** @@ -7105,6 +7107,17 @@ 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; + /** + * Only used for {@link yieldsToGroupIds}. + * Multiple providers can have the same group id. + */ + groupId?: InlineCompletionProviderGroupId; + /** + * Returns a list of preferred provider {@link groupId}s. + * The current provider is only requested for completions if no provider with a preferred group id returned a result. + */ + yieldsToGroupIds?: InlineCompletionProviderGroupId[]; + toString?(): string; } export interface CodeAction { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c34ff9c91da..732637669fb 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -557,7 +557,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, this._languageFeaturesService.completionProvider.register(selector, provider)); } - $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean): void { + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean, extensionId: string, yieldsToExtensionIds: string[]): void { const provider: languages.InlineCompletionsProvider = { provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); @@ -574,6 +574,11 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { this._proxy.$freeInlineCompletionsList(handle, completions.pid); + }, + groupId: extensionId, + yieldsToGroupIds: yieldsToExtensionIds, + toString() { + return `InlineCompletionsProvider(${extensionId})`; } }; this._registrations.set(handle, this._languageFeaturesService.inlineCompletionsProvider.register(selector, provider)); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 56f66abde2f..1e585c0d3b0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -578,14 +578,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerCompletionItemProvider(extension, checkSelector(selector), provider, triggerCharacters); }, - registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { + registerInlineCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider, metadata?: vscode.InlineCompletionItemProviderMetadata): vscode.Disposable { if (provider.handleDidShowCompletionItem) { checkProposedApiEnabled(extension, 'inlineCompletionsAdditions'); } if (provider.handleDidPartiallyAcceptCompletionItem) { checkProposedApiEnabled(extension, 'inlineCompletionsAdditions'); } - return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider); + if (metadata) { + checkProposedApiEnabled(extension, 'inlineCompletionsAdditions'); + } + return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider, metadata); }, registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentLinkProvider(extension, checkSelector(selector), provider); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c8b82a40e92..413980ad9b3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -407,7 +407,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; - $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean): void; + $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleDidShowCompletionItem: boolean, extensionId: string, yieldsToExtensionIds: string[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; $emitInlayHintsEvent(eventHandle: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 8c871feda78..78ecbea0a85 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -2249,10 +2249,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- ghost test - registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider): vscode.Disposable { + registerInlineCompletionsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProvider, metadata: vscode.InlineCompletionItemProviderMetadata | undefined): vscode.Disposable { const adapter = new InlineCompletionAdapter(extension, this._documents, provider, this._commands.converter); const handle = this._addNewAdapter(adapter, extension); - this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector, extension), adapter.supportsHandleEvents); + this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector, extension), adapter.supportsHandleEvents, + ExtensionIdentifier.toKey(extension.identifier.value), metadata?.yieldTo?.map(extId => ExtensionIdentifier.toKey(extId)) || []); return this._createDisposable(handle); } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 8412aca06ed..c38c4e23671 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -7,6 +7,22 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima + export namespace languages { + /** + * Registers an inline completion provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline completion provider. + * @param metadata Metadata about the provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider, metadata: InlineCompletionItemProviderMetadata): Disposable; + } + export interface InlineCompletionItem { /** * If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed. @@ -15,6 +31,14 @@ declare module 'vscode' { completeBracketPairs?: boolean; } + export interface InlineCompletionItemProviderMetadata { + /** + * Specifies a list of extension ids that this provider yields to if they return a result. + * If some inline completion provider registered by such an extension returns a result, this provider is not asked. + */ + yieldTo: string[]; + } + export interface InlineCompletionItemProvider { /** * @param completionItem The completion item that was shown.