diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 3659925bf75..fedc753fb66 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -833,6 +833,10 @@ export interface InlineCompletion { export interface InlineCompletions { readonly items: readonly TItem[]; + /** + * A list of commands associated with the inline completions of this list. + */ + readonly commands?: Command[]; } export interface InlineCompletionsProvider { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts index 7ab26922483..6a2fe0eb945 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts @@ -20,6 +20,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; +import { Command } from 'vs/editor/common/languages'; export class InlineCompletionsHover implements IHoverPart { constructor( @@ -39,6 +40,10 @@ export class InlineCompletionsHover implements IHoverPart { public hasMultipleSuggestions(): Promise { return this.controller.hasMultipleInlineCompletions(); } + + public get commands(): Command[] { + return this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value?.commands || []; + } } export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant { @@ -100,6 +105,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan this.renderScreenReaderText(context, part, disposableStore); } + // TODO@hediet: deprecate MenuId.InlineCompletionsActions const menu = disposableStore.add(this._menuService.createMenu( MenuId.InlineCompletionsActions, this._contextKeyService @@ -131,6 +137,14 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan } }); + for (const command of part.commands) { + context.statusBar.addAction({ + label: command.title, + commandId: command.id, + run: () => this._commandService.executeCommand(command.id, ...(command.arguments || [])) + }); + } + for (const [_, group] of menu.getActions()) { for (const action of group) { if (action instanceof MenuItemAction) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index fb8393f665a..bccd1734dbf 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -15,7 +15,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { Command, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; import { BaseGhostTextWidgetModel, GhostText, GhostTextReplacement, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; @@ -543,6 +543,11 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { this.onDidChangeEmitter.fire(); } + + public get commands(): Command[] { + const lists = new Set(this.cache.value?.completions.map(c => c.inlineCompletion.sourceInlineCompletions) || []); + return [...lists].flatMap(l => l.commands || []); + } } export class UpdateOperation implements IDisposable { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c3dab93ceb4..57527a3afa3 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6332,6 +6332,10 @@ declare namespace monaco.languages { export interface InlineCompletions { readonly items: readonly TItem[]; + /** + * A list of commands associated with the inline completions of this list. + */ + readonly commands?: Command[]; } export interface InlineCompletionsProvider { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 2cc57319c69..c53758c8946 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1030,6 +1030,10 @@ class InlineCompletionAdapterBase { public async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { return undefined; } + + public disposeCompletions(pid: number): void { } + + public handleDidShowCompletionItem(pid: number, idx: number): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1104,7 +1108,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } - public disposeCompletions(pid: number) { + public override disposeCompletions(pid: number) { this._cache.delete(pid); const d = this._disposables.get(pid); if (d) { @@ -1113,7 +1117,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { this._disposables.delete(pid); } - public handleDidShowCompletionItem(pid: number, idx: number): void { + public override handleDidShowCompletionItem(pid: number, idx: number): void { const completionItem = this._cache.get(pid, idx); if (completionItem) { InlineCompletionController.get(this._provider).fireOnDidShowCompletionItem({ @@ -1124,8 +1128,10 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { - private readonly _cache = new Cache('InlineCompletionItemNew'); - private readonly _disposables = new Map(); + private readonly _references = new ReferenceMap<{ + dispose(): void; + items: readonly vscode.InlineCompletionItemNew[]; + }>(); private readonly isAdditionProposedApiEnabled = isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions'); @@ -1170,9 +1176,17 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { } const normalizedResult = isArray(result) ? result : result.items; + const commands = isArray(result) ? [] : result.commands || []; - const pid = this._cache.add(normalizedResult); let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + if (disposableStore) { + disposableStore.dispose(); + } + }, + items: normalizedResult + }); return { pid, @@ -1181,7 +1195,6 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { if (item.command) { if (!disposableStore) { disposableStore = new DisposableStore(); - this._disposables.set(pid, disposableStore); } command = this._commands.toInternal(item.command, disposableStore); } @@ -1196,28 +1209,51 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { completeBracketPairs: this.isAdditionProposedApiEnabled ? item.completeBracketPairs : false }); }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }) }; } - public disposeCompletions(pid: number) { - this._cache.delete(pid); - const d = this._disposables.get(pid); - if (d) { - d.clear(); - } - this._disposables.delete(pid); + public override disposeCompletions(pid: number) { + const data = this._references.disposeReferenceId(pid); + data?.dispose(); } - public handleDidShowCompletionItem(pid: number, idx: number): void { - const completionItem = this._cache.get(pid, idx); + public override handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { - if (this._provider.handleDidShowCompletionItem && isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions')) { + if (this._provider.handleDidShowCompletionItem && this.isAdditionProposedApiEnabled) { this._provider.handleDidShowCompletionItem(completionItem); } } } } +class ReferenceMap { + private readonly _references = new Map(); + private _idPool = 1; + + createReferenceId(value: T): number { + const id = this._idPool++; + this._references.set(id, value); + return id; + } + + disposeReferenceId(referenceId: number): T | undefined { + const value = this._references.get(referenceId); + this._references.delete(referenceId); + return value; + } + + get(referenceId: number): T | undefined { + return this._references.get(referenceId); + } +} + export class InlineCompletionController implements vscode.InlineCompletionController { private static readonly map = new WeakMap, InlineCompletionController>(); @@ -2194,13 +2230,13 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx); }, undefined, undefined); } $freeInlineCompletionsList(handle: number, pid: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); } // --- parameter hints diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 8ed1d005b1a..a69e00f05ec 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1643,8 +1643,11 @@ export class InlineSuggestionNew implements vscode.InlineCompletionItemNew { export class InlineSuggestionsNew implements vscode.InlineCompletionListNew { items: vscode.InlineCompletionItemNew[]; - constructor(items: vscode.InlineCompletionItemNew[]) { + commands: vscode.Command[] | undefined; + + constructor(items: vscode.InlineCompletionItemNew[], commands?: vscode.Command[]) { this.items = items; + this.commands = commands; } } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts index 200b7ebda04..8ab631571c8 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts @@ -18,28 +18,35 @@ declare module 'vscode' { * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A inline completion provider. + * @param provider An inline completion provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerInlineCompletionItemProviderNew(selector: DocumentSelector, provider: InlineCompletionItemProviderNew): Disposable; } - // TODO@API doc + /** + * The inline completion item provider interface defines the contract between extensions and + * the inline completion feature. + * + * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. + */ export interface InlineCompletionItemProviderNew { /** * Provides inline completion items for the given position and document. * If inline completions are enabled, this method will be called whenever the user stopped typing. * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion. - * Use `context.triggerKind` to distinguish between these scenarios. + * `context.triggerKind` can be used to distinguish between these scenarios. */ provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContextNew, token: CancellationToken): ProviderResult; } - // TODO@API doc + /** + * Provides information about the context in which an inline completion was requested. + */ export interface InlineCompletionContextNew { /** - * How the completion was triggered. + * Describes how the inline completion was triggered. */ readonly triggerKind: InlineCompletionTriggerKindNew; @@ -52,21 +59,27 @@ declare module 'vscode' { * the inline completion must also replace `.` and start with `.log`, for example `.log()`. * * Inline completion providers are requested again whenever the selected item changes. - * - * The user must configure `"editor.suggest.preview": true` for this feature. */ readonly selectedCompletionInfo: SelectedCompletionInfoNew | undefined; } - // TODO@API find a better name, xyzFilter, xyzConstraint - // TODO@API doc + /** + * Describes the currently selected completion item. + */ export interface SelectedCompletionInfoNew { + /** + * The range that will be replaced if this completion item is accepted. + */ range: Range; + + /** + * The text the range will be replaced with if this completion is accepted. + */ text: string; } /** - * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. + * Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. */ export enum InlineCompletionTriggerKindNew { /** @@ -82,22 +95,29 @@ declare module 'vscode' { Automatic = 1, } - // TODO@API doc + /** + * Represents a collection of {@link InlineCompletionItemNew inline completion items} to be presented + * in the editor. + */ export class InlineCompletionListNew { + /** + * The inline completion items. + */ items: InlineCompletionItemNew[]; - // TODO@API We could keep this and allow for `vscode.Command` instances that explain - // the result. That would replace the existing proposed menu-identifier and be more LSP friendly - // TODO@API maybe use MarkdownString - // commands?: Command[]; // "Show More..." - // description: MarkdownString - /** - * @deprecated Return an array of Inline Completion items directly. Will be removed eventually. - */ - constructor(items: InlineCompletionItemNew[]); + * A list of commands associated with the inline completions of this list. + */ + commands?: Command[]; + + constructor(items: InlineCompletionItemNew[], commands?: Command[]); } + /** + * An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. + * + * @see {@link InlineCompletionItemProviderNew.provideInlineCompletionItems} + */ export class InlineCompletionItemNew { /** * The text to replace the range with. Must be set. @@ -113,7 +133,7 @@ declare module 'vscode' { /** * A text that is used to decide if this inline completion should be shown. - * An inline completion is shown if the text to replace is a subword of the filter text. + * An inline completion is shown if the text to replace is a prefix of the filter text. */ filterText?: string; @@ -133,7 +153,13 @@ declare module 'vscode' { */ command?: Command; - // TODO@API doc + /** + * Creates a new inline completion item. + * + * @param insertText The text to replace the range with. + * @param range The range to replace. If not set, the word at the requested position will be used. + * @param command An optional {@link Command} that is executed *after* inserting this completion. + */ constructor(insertText: string | SnippetString, range?: Range, command?: Command); } }