diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 1ffc4cdc6ca92..d12d1d7a8f32d 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -123,6 +123,7 @@ import { WebviewsExtImpl } from './webviews'; import { TasksExtImpl } from './tasks/tasks'; import { DebugExtImpl } from './node/debug/debug'; import { FileSystemExtImpl } from './file-system'; +import { QuickPick, QuickPickItem } from '@theia/plugin'; export function createAPIFactory( rpc: RPCProtocol, @@ -271,6 +272,9 @@ export function createAPIFactory( return quickOpenExt.showQuickPick(items, options); } }, + createQuickPick(): QuickPick { + return quickOpenExt.createQuickPick(); + }, showWorkspaceFolderPick(options?: theia.WorkspaceFolderPickOptions): PromiseLike { return workspaceExt.pickWorkspaceFolder(options); }, diff --git a/packages/plugin-ext/src/plugin/quick-open.ts b/packages/plugin-ext/src/plugin/quick-open.ts index 3602b34fb370b..21a7488733c13 100644 --- a/packages/plugin-ext/src/plugin/quick-open.ts +++ b/packages/plugin-ext/src/plugin/quick-open.ts @@ -19,6 +19,9 @@ import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { RPCProtocol } from '../api/rpc-protocol'; import { anyPromise } from '../api/async-util'; import { hookCancellationToken } from '../api/async-util'; +import { Emitter, Event } from '@theia/core/lib/common/event'; +import { QuickPick, QuickInputButton } from '@theia/plugin'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; export type Item = string | QuickPickItem; @@ -109,6 +112,10 @@ export class QuickOpenExtImpl implements QuickOpenExt { return hookCancellationToken(token, promise); } + createQuickPick(): QuickPick { + return new QuickPickExt(this); + } + showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): PromiseLike { this.validateInputHandler = options && options.validateInput; @@ -123,3 +130,128 @@ export class QuickOpenExtImpl implements QuickOpenExt { } } + +/** + * Base implementation of {@link QuickPick} that uses {@link QuickOpenExt}. + * Missing functionality is going to be implemented in the scope of https://github.com/theia-ide/theia/issues/5059 + */ +export class QuickPickExt implements QuickPick { + + busy: boolean; + buttons: ReadonlyArray; + canSelectMany: boolean; + enabled: boolean; + ignoreFocusOut: boolean; + matchOnDescription: boolean; + matchOnDetail: boolean; + selectedItems: ReadonlyArray; + step: number | undefined; + title: string | undefined; + totalSteps: number | undefined; + value: string; + + private _items: T[]; + private _activeItems: T[]; + private _placeholder: string | undefined; + private disposableCollection: DisposableCollection; + private readonly onDidHideEmitter: Emitter; + private readonly onDidAcceptEmitter: Emitter; + private readonly onDidChangeActiveEmitter: Emitter; + private readonly onDidChangeSelectionEmitter: Emitter; + private readonly onDidChangeValueEmitter: Emitter; + private readonly onDidTriggerButtonEmitter: Emitter; + + constructor(readonly quickOpen: QuickOpenExtImpl) { + this._items = []; + this._activeItems = []; + this._placeholder = ''; + this.buttons = []; + this.step = 0; + this.title = ''; + this.totalSteps = 0; + this.value = ''; + this.disposableCollection = new DisposableCollection(); + this.disposableCollection.push(this.onDidHideEmitter = new Emitter()); + this.disposableCollection.push(this.onDidAcceptEmitter = new Emitter()); + this.disposableCollection.push(this.onDidChangeActiveEmitter = new Emitter()); + this.disposableCollection.push(this.onDidChangeSelectionEmitter = new Emitter()); + this.disposableCollection.push(this.onDidChangeValueEmitter = new Emitter()); + this.disposableCollection.push(this.onDidTriggerButtonEmitter = new Emitter()); + } + + get items(): T[] { + return this._items; + } + + set items(activeItems: T[]) { + this._items = activeItems; + } + + get activeItems(): T[] { + return this._activeItems; + } + + set activeItems(activeItems: T[]) { + this._activeItems = activeItems; + } + + get onDidAccept(): Event { + return this.onDidAcceptEmitter.event; + } + + get placeholder(): string | undefined { + return this._placeholder; + } + set placeholder(placeholder: string | undefined) { + this._placeholder = placeholder; + } + + get onDidChangeActive(): Event { + return this.onDidChangeActiveEmitter.event; + } + + get onDidChangeSelection(): Event { + return this.onDidChangeSelectionEmitter.event; + } + + get onDidChangeValue(): Event { + return this.onDidChangeValueEmitter.event; + } + + get onDidTriggerButton(): Event { + return this.onDidTriggerButtonEmitter.event; + } + + get onDidHide(): Event { + return this.onDidHideEmitter.event; + } + + dispose(): void { + this.disposableCollection.dispose(); + } + + hide(): void { + this.dispose(); + } + + show(): void { + const hide = () => { + this.onDidHideEmitter.fire(undefined); + }; + const selectItem = (item: T) => { + this.activeItems = [item]; + this.onDidAcceptEmitter.fire(undefined); + this.onDidChangeSelectionEmitter.fire([item]); + }; + this.quickOpen.showQuickPick(this.items.map(item => item as T), { + // tslint:disable-next-line:no-any + onDidSelectItem(item: T | string): any { + if (typeof item !== 'string') { + selectItem(item); + } + hide(); + }, placeHolder: this.placeholder + }); + } + +} diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index b41449813b06c..3f84cc6498b83 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -1852,6 +1852,96 @@ declare module '@theia/plugin' { provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult; } + /** + * A light-weight user input UI that is initially not visible. After + * configuring it through its properties the extension can make it + * visible by calling [QuickInput.show](#QuickInput.show). + * + * There are several reasons why this UI might have to be hidden and + * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). + * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), + * the user pressing Esc, some other input UI opening, etc.) + * + * A user pressing Enter or some other gesture implying acceptance + * of the current state does not automatically hide this UI component. + * It is up to the extension to decide whether to accept the user's input + * and if the UI should indeed be hidden through a call to [QuickInput.hide](#QuickInput.hide). + * + * When the extension no longer needs this input UI, it should + * [QuickInput.dispose](#QuickInput.dispose) it to allow for freeing up + * any resources associated with it. + * + * See [QuickPick](#QuickPick) and [InputBox](#InputBox) for concrete UIs. + */ + export interface QuickInput { + + /** + * An optional title. + */ + title: string | undefined; + + /** + * An optional current step count. + */ + step: number | undefined; + + /** + * An optional total step count. + */ + totalSteps: number | undefined; + + /** + * If the UI should allow for user input. Defaults to true. + * + * Change this to false, e.g., while validating user input or + * loading data for the next step in user input. + */ + enabled: boolean; + + /** + * If the UI should show a progress indicator. Defaults to false. + * + * Change this to true, e.g., while loading more data or validating + * user input. + */ + busy: boolean; + + /** + * If the UI should stay open even when loosing UI focus. Defaults to false. + */ + ignoreFocusOut: boolean; + + /** + * Makes the input UI visible in its current configuration. Any other input + * UI will first fire an [QuickInput.onDidHide](#QuickInput.onDidHide) event. + */ + show(): void; + + /** + * Hides this input UI. This will also fire an [QuickInput.onDidHide](#QuickInput.onDidHide) + * event. + */ + hide(): void; + + /** + * An event signaling when this input UI is hidden. + * + * There are several reasons why this UI might have to be hidden and + * the extension will be notified through [QuickInput.onDidHide](#QuickInput.onDidHide). + * (Examples include: an explicit call to [QuickInput.hide](#QuickInput.hide), + * the user pressing Esc, some other input UI opening, etc.) + */ + onDidHide: Event; + + /** + * Dispose of this input UI and any associated resources. If it is still + * visible, it is first hidden. After this call the input UI is no longer + * functional and no additional methods or properties on it should be + * accessed. Instead a new input UI should be created. + */ + dispose(): void; + } + /** * Something that can be selected from a list of items. */ @@ -1879,6 +1969,105 @@ declare module '@theia/plugin' { picked?: boolean; } + /** + * Button for an action in a [QuickPick](#QuickPick) or [InputBox](#InputBox). + */ + export interface QuickInputButton { + + /** + * Icon for the button. + */ + readonly iconPath: Uri | { light: Uri; dark: Uri } | ThemeIcon; + + /** + * An optional tooltip. + */ + readonly tooltip?: string | undefined; + } + + /** + * A concrete [QuickInput](#QuickInput) to let the user pick an item from a + * list of items of type T. The items can be filtered through a filter text field and + * there is an option [canSelectMany](#QuickPick.canSelectMany) to allow for + * selecting multiple items. + * + * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + */ + export interface QuickPick extends QuickInput { + + /** + * Current value of the filter text. + */ + value: string; + + /** + * Optional placeholder in the filter text. + */ + placeholder: string | undefined; + + /** + * An event signaling when the value of the filter text has changed. + */ + readonly onDidChangeValue: Event; + + /** + * An event signaling when the user indicated acceptance of the selected item(s). + */ + readonly onDidAccept: Event; + + /** + * Buttons for actions in the UI. + */ + buttons: ReadonlyArray; + + /** + * An event signaling when a button was triggered. + */ + readonly onDidTriggerButton: Event; + + /** + * Items to pick from. + */ + items: ReadonlyArray; + + /** + * If multiple items can be selected at the same time. Defaults to false. + */ + canSelectMany: boolean; + + /** + * If the filter text should also be matched against the description of the items. Defaults to false. + */ + matchOnDescription: boolean; + + /** + * If the filter text should also be matched against the detail of the items. Defaults to false. + */ + matchOnDetail: boolean; + + /** + * Active items. This can be read and updated by the extension. + */ + activeItems: ReadonlyArray; + + /** + * An event signaling when the active items have changed. + */ + readonly onDidChangeActive: Event; + + /** + * Selected items. This can be read and updated by the extension. + */ + selectedItems: ReadonlyArray; + + /** + * An event signaling when the selected items have changed. + */ + readonly onDidChangeSelection: Event; + } + /** * Options for configuration behavior of the quick pick */ @@ -2885,6 +3074,18 @@ declare module '@theia/plugin' { token?: CancellationToken ): PromiseLike; + /** + * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list + * of items of type T. + * + * Note that in many cases the more convenient [window.showQuickPick](#window.showQuickPick) + * is easier to use. [window.createQuickPick](#window.createQuickPick) should be used + * when [window.showQuickPick](#window.showQuickPick) does not offer the required flexibility. + * + * @return A new [QuickPick](#QuickPick). + */ + export function createQuickPick(): QuickPick; + /** * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. * Returns `undefined` if no folder is open.