From f192f494a6b96b886f102c398c9a3dce510bda6e Mon Sep 17 00:00:00 2001 From: Igor Vinokur Date: Fri, 17 May 2019 11:33:39 +0300 Subject: [PATCH] [Plugin-Api] Apply window.createInput() function Signed-off-by: Igor Vinokur --- .../browser/quick-open/quick-input-service.ts | 7 + .../src/browser/monaco-quick-open-service.ts | 1 + packages/plugin-ext/src/api/plugin-api.ts | 1 + .../src/main/browser/quick-open-main.ts | 1 + .../plugin-ext/src/plugin/plugin-context.ts | 9 +- packages/plugin-ext/src/plugin/quick-open.ts | 90 +++++++- packages/plugin-ext/src/plugin/types-impl.ts | 9 + packages/plugin/src/theia.d.ts | 196 ++++++++++++++++++ 8 files changed, 310 insertions(+), 4 deletions(-) diff --git a/packages/core/src/browser/quick-open/quick-input-service.ts b/packages/core/src/browser/quick-open/quick-input-service.ts index 992288dedeaed..222b430d1c77c 100644 --- a/packages/core/src/browser/quick-open/quick-input-service.ts +++ b/packages/core/src/browser/quick-open/quick-input-service.ts @@ -20,6 +20,7 @@ import { QuickOpenItem, QuickOpenMode } from './quick-open-model'; import { Deferred } from '../../common/promise-util'; import { MaybePromise } from '../../common/types'; import { MessageType } from '../../common/message-service-protocol'; +import { Emitter, Event } from '../../common/event'; export interface QuickInputOptions { /** @@ -83,6 +84,7 @@ export class QuickInputService { run: mode => { if (!error && mode === QuickOpenMode.OPEN) { result.resolve(currentText); + this.onDidAcceptEmitter.fire(undefined); return true; } return false; @@ -105,4 +107,9 @@ export class QuickInputService { return prompt ? `${prompt} (${this.defaultPrompt})` : this.defaultPrompt; } + readonly onDidAcceptEmitter: Emitter = new Emitter(); + get onDidAccept(): Event { + return this.onDidAcceptEmitter.event; + } + } diff --git a/packages/monaco/src/browser/monaco-quick-open-service.ts b/packages/monaco/src/browser/monaco-quick-open-service.ts index 554b65fdb31d4..f4e02614027ca 100644 --- a/packages/monaco/src/browser/monaco-quick-open-service.ts +++ b/packages/monaco/src/browser/monaco-quick-open-service.ts @@ -59,6 +59,7 @@ export class MonacoQuickOpenService extends QuickOpenService { container.style.position = 'absolute'; container.style.top = '0px'; container.style.right = '50%'; + container.style.zIndex = '1000000'; overlayWidgets.appendChild(container); } diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index fbddbcddbc0d5..745eed6b29b21 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -283,6 +283,7 @@ export interface StatusBarMessageRegistryMain { export interface QuickOpenExt { $onItemSelected(handle: number): void; $validateInput(input: string): PromiseLike | undefined; + $acceptInput(): Promise; } /** diff --git a/packages/plugin-ext/src/main/browser/quick-open-main.ts b/packages/plugin-ext/src/main/browser/quick-open-main.ts index e1f82a3e04ce1..fa36602456b00 100644 --- a/packages/plugin-ext/src/main/browser/quick-open-main.ts +++ b/packages/plugin-ext/src/main/browser/quick-open-main.ts @@ -37,6 +37,7 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT); this.delegate = container.get(MonacoQuickOpenService); this.quickInput = container.get(QuickInputService); + this.quickInput.onDidAccept(() => this.proxy.$acceptInput()); } private cleanUp() { diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index d12d1d7a8f32d..1991d45f08126 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -98,7 +98,8 @@ import { ColorPresentation, OperatingSystem, WebviewPanelTargetArea, - FileSystemError + FileSystemError, + QuickInputButtons } from './types-impl'; import { SymbolKind } from '../api/model'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; @@ -346,6 +347,9 @@ export function createAPIFactory( console.error('Progress location \'SourceControl\' is not supported.'); }); } + }, + createInputBox(): theia.InputBox { + return quickOpenExt.createInputBox(); } }; @@ -725,7 +729,8 @@ export function createAPIFactory( FoldingRangeKind, OperatingSystem, WebviewPanelTargetArea, - FileSystemError + FileSystemError, + QuickInputButtons }; }; } diff --git a/packages/plugin-ext/src/plugin/quick-open.ts b/packages/plugin-ext/src/plugin/quick-open.ts index 21a7488733c13..4618a161ed9d0 100644 --- a/packages/plugin-ext/src/plugin/quick-open.ts +++ b/packages/plugin-ext/src/plugin/quick-open.ts @@ -14,13 +14,12 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { QuickOpenExt, PLUGIN_RPC_CONTEXT as Ext, QuickOpenMain, PickOpenItem } from '../api/plugin-api'; -import { QuickPickOptions, QuickPickItem, InputBoxOptions } from '@theia/plugin'; +import { QuickPickOptions, QuickPickItem, InputBoxOptions, InputBox, QuickInputButton, QuickPick } from '@theia/plugin'; 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; @@ -29,9 +28,11 @@ export class QuickOpenExtImpl implements QuickOpenExt { private proxy: QuickOpenMain; private selectItemHandler: undefined | ((handle: number) => void); private validateInputHandler: undefined | ((input: string) => string | PromiseLike | undefined); + private onDidAcceptInputEmitter: Emitter; constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(Ext.QUICK_OPEN_MAIN); + this.onDidAcceptInputEmitter = new Emitter(); } $onItemSelected(handle: number): void { if (this.selectItemHandler) { @@ -129,6 +130,91 @@ export class QuickOpenExtImpl implements QuickOpenExt { return hookCancellationToken(token, promise); } + createInputBox(): InputBox { + return new InputBoxExt(this, this.onDidAcceptInputEmitter); + } + + async $acceptInput(): Promise { + this.onDidAcceptInputEmitter.fire(undefined); + } + +} + +/** + * Base implementation of {@link InputBox} that uses {@link QuickOpenExt}. + * Missing functionality is going to be implemented in the scope of https://github.com/theia-ide/theia/issues/5109 + */ +export class InputBoxExt implements InputBox { + + busy: boolean; + buttons: ReadonlyArray; + enabled: boolean; + ignoreFocusOut: boolean; + password: boolean; + placeholder: string | undefined; + prompt: string | undefined; + step: number | undefined; + title: string | undefined; + totalSteps: number | undefined; + validationMessage: string | undefined; + value: string; + + private readonly disposables: DisposableCollection; + private readonly onDidChangeValueEmitter: Emitter; + private readonly onDidHideEmitter: Emitter; + private readonly onDidTriggerButtonEmitter: Emitter; + + constructor(readonly quickOpen: QuickOpenExtImpl, readonly onDidAcceptEmitter: Emitter) { + this.disposables = new DisposableCollection(); + this.disposables.push(this.onDidChangeValueEmitter = new Emitter()); + this.disposables.push(this.onDidHideEmitter = new Emitter()); + this.disposables.push(this.onDidTriggerButtonEmitter = new Emitter()); + } + + get onDidChangeValue(): Event { + return this.onDidChangeValueEmitter.event; + } + + get onDidAccept(): Event { + return this.onDidAcceptEmitter.event; + } + + get onDidHide(): Event { + return this.onDidHideEmitter.event; + } + + get onDidTriggerButton(): Event { + return this.onDidTriggerButtonEmitter.event; + } + + dispose(): void { + this.disposables.dispose(); + } + + hide(): void { + this.dispose(); + } + + show(): void { + const update = (value: string) => { + this.onDidChangeValueEmitter.fire(value); + if (this.validationMessage && this.validationMessage.length > 0) { + return this.validationMessage; + } + }; + this.quickOpen.showInput({ + password: this.password, + placeHolder: this.placeholder, + prompt: this.prompt, + value: this.value, + ignoreFocusOut: this.ignoreFocusOut, + validateInput(value: string): string | undefined { + if (value.length > 0) { + return update(value); + } + } + }); + } } /** diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 0910d9d1f82a5..4bfe4706ec041 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1226,6 +1226,15 @@ export enum FileChangeType { Deleted = 3, } +export interface QuickInputButton { + readonly iconPath: ThemeIcon; + readonly tooltip?: string | undefined; +} + +export class QuickInputButtons { + static readonly Back: QuickInputButton; +} + export class FileSystemError extends Error { static FileExists(messageOrUri?: string | URI): FileSystemError { diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 3f84cc6498b83..9395770b923fe 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -2167,6 +2167,11 @@ declare module '@theia/plugin' { * Return `undefined`, or the empty string when 'value' is valid. */ validateInput?(value: string): string | undefined | PromiseLike; + + /** + * An optional function that will be called on Enter key. + */ + onAccept?(): void; } /** @@ -3392,6 +3397,197 @@ declare module '@theia/plugin' { * @return The thenable the task-callback returned. */ export function withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => PromiseLike): PromiseLike; + + /** + * Creates a [InputBox](#InputBox) to let the user enter some text input. + * + * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) + * is easier to use. [window.createInputBox](#window.createInputBox) should be used + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + * + * @return A new [InputBox](#InputBox). + */ + export function createInputBox(): InputBox; + } + + /** + * Predefined buttons for [QuickPick](#QuickPick) and [InputBox](#InputBox). + */ + export class QuickInputButtons { + + /** + * A back button for [QuickPick](#QuickPick) and [InputBox](#InputBox). + * + * When a navigation 'back' button is needed this one should be used for consistency. + * It comes with a predefined icon, tooltip and location. + */ + static readonly Back: QuickInputButton; + + /** + * @hidden + */ + private constructor(); + } + + /** + * A concrete [QuickInput](#QuickInput) to let the user input a text value. + * + * Note that in many cases the more convenient [window.showInputBox](#window.showInputBox) + * is easier to use. [window.createInputBox](#window.createInputBox) should be used + * when [window.showInputBox](#window.showInputBox) does not offer the required flexibility. + */ + export interface InputBox extends QuickInput { + + /** + * Current input value. + */ + value: string; + + /** + * Optional placeholder in the filter text. + */ + placeholder: string | undefined; + + /** + * If the input value should be hidden. Defaults to false. + */ + password: boolean; + + /** + * An event signaling when the value has changed. + */ + readonly onDidChangeValue: Event; + + /** + * An event signaling when the user indicated acceptance of the input value. + */ + readonly onDidAccept: Event; + + /** + * Buttons for actions in the UI. + */ + buttons: ReadonlyArray; + + /** + * An event signaling when a button was triggered. + */ + readonly onDidTriggerButton: Event; + + /** + * An optional prompt text providing some ask or explanation to the user. + */ + prompt: string | undefined; + + /** + * An optional validation message indicating a problem with the current input value. + */ + validationMessage: string | undefined; + } + + /** + * 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; + } + + /** + * 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; } /** * Value-object describing where and how progress should show.