Skip to content

Commit

Permalink
eclipse-che/che#9980 implement 'showInputBox' window API function
Browse files Browse the repository at this point in the history
Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com>
  • Loading branch information
evidolob committed Jun 25, 2018
1 parent 422a328 commit 77141d3
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 19 deletions.
3 changes: 3 additions & 0 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory';
import { MonacoStrictEditorTextFocusContext } from './monaco-keybinding-contexts';
import { MonacoFrontendApplicationContribution } from './monaco-frontend-application-contribution';
import MonacoTextmateModuleBinder from './textmate/monaco-textmate-frontend-bindings';
import { QuickInputService } from './monaco-quick-input-service';

decorate(injectable(), MonacoToProtocolConverter);
decorate(injectable(), ProtocolToMonacoConverter);
Expand Down Expand Up @@ -91,4 +92,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
).inSingletonScope();

MonacoTextmateModuleBinder(bind, unbind, isBound, rebind);

bind(QuickInputService).toSelf().inSingletonScope();
});
173 changes: 173 additions & 0 deletions packages/monaco/src/browser/monaco-quick-input-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { inject, injectable } from "inversify";
import { MonacoQuickOpenService, MonacoQuickOpenControllerOptsImpl } from "./monaco-quick-open-service";
import { QuickOpenMode, QuickOpenItemOptions, QuickOpenModel, QuickOpenOptions, QuickOpenItem } from "@theia/core/lib/browser";

export interface QuickInputOptions {

/**
* The prefill value.
*/
value?: string;

/**
* The text to display under the input box.
*/
prompt?: string;

/**
* The place holder in the input box to guide the user what to type.
*/
placeHolder?: string;

/**
* Set to `true` to show a password prompt that will not show the typed value.
*/
password?: boolean;

/**
* Set to `true` to keep the input box open when focus moves to another part of the editor or to another window.
*/
ignoreFocusOut?: boolean;

/**
* An optional function that will be called to validate input and to give a hint
* to the user.
*
* @param value The current value of the input box.
* @return Return `undefined`, or the empty string when 'value' is valid.
*/
validateInput?(value: string): string | undefined | PromiseLike<string | undefined>;
}

const promptMessage = "Press 'Enter' to confirm your input or 'Escape' to cancel";

@injectable()
export class QuickInputService {

private opts: MonacoQuickInputControllerOptsImpl;
@inject(MonacoQuickOpenService)
protected readonly quickOpenService: MonacoQuickOpenService;

open(options: QuickInputOptions): Promise<string | undefined> {
options.prompt = this.createPrompt(options.prompt);

const inputItem = new InputOpenItemOptions(options.prompt);
this.opts = new MonacoQuickInputControllerOptsImpl({
onType: (s, a) => this.validateInput(s, a, inputItem)
},
options,
{
prefix: options.value,
placeholder: options.placeHolder,
onClose: () => inputItem.resolve(undefined)
});
this.quickOpenService.internalOpen(this.opts);

return new Promise(r => {
inputItem.resolve = r;
});
}

private createPrompt(prompt?: string): string {
if (prompt) {
return `${prompt} (${promptMessage})`;
} else {
return promptMessage;
}
}

private validateInput(str: string, acceptor: (items: QuickOpenItem[]) => void, inputItem: InputOpenItemOptions): void {
inputItem.currentText = str;
acceptor([new QuickOpenItem(inputItem)]);
if (this.opts && this.opts.validateInput) {
const hint = this.opts.validateInput(str);
if (hint) {
if (typeof hint !== 'string') {
hint.then(p => {
if (p) {
this.setValidationState(inputItem, p, false);
} else {
this.setValidationState(inputItem, this.opts!.prompt!, true);
}
});
} else {
this.setValidationState(inputItem, hint, false);
}
} else {
this.setValidationState(inputItem, this.opts.prompt!, true);
}
}
}

private setValidationState(inputItem: InputOpenItemOptions, label: string, isValid: boolean): void {
this.quickOpenService.clearInputDecoration();
inputItem.isValid = isValid;
inputItem.label = label;
// this.widget.refresh();
if (isValid) {
this.quickOpenService.clearInputDecoration();
} else {
this.quickOpenService.showInputDecoration(monaco.Severity.Error);
}
}
}

class InputOpenItemOptions implements QuickOpenItemOptions {
currentText: string;
isValid = true;
resolve: (value?: string | PromiseLike<string> | undefined) => void;

constructor(
public label?: string) {
}

run(mode: QuickOpenMode): boolean {
if (this.isValid && mode === QuickOpenMode.OPEN) {
this.resolve(this.currentText);
return true;
}
return false;
}
}

class MonacoQuickInputControllerOptsImpl extends MonacoQuickOpenControllerOptsImpl {
readonly prompt?: string;
readonly password?: boolean;
readonly ignoreFocusOut?: boolean;
validateInput?(value: string): string | undefined | PromiseLike<string | undefined>;
constructor(
model: QuickOpenModel,
inputOptions: QuickInputOptions,
options?: QuickOpenOptions
) {
super(model, options);
if (inputOptions.password) {
this.password = inputOptions.password;
}
if (inputOptions.prompt) {
this.prompt = inputOptions.prompt;
}

if (inputOptions.ignoreFocusOut) {
this.ignoreFocusOut = inputOptions.ignoreFocusOut;
}
if (inputOptions.validateInput) {
this.validateInput = inputOptions.validateInput;
}
}
}
17 changes: 16 additions & 1 deletion packages/monaco/src/browser/monaco-quick-open-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { ILogger } from '@theia/core';

export interface MonacoQuickOpenControllerOpts extends monaco.quickOpen.IQuickOpenControllerOpts {
readonly prefix?: string;
readonly password?: boolean;
readonly ignoreFocusOut?: boolean;
onType?(lookFor: string, acceptor: (model: monaco.quickOpen.QuickOpenModel) => void): void;
onClose?(canceled: boolean): void;
}
Expand Down Expand Up @@ -59,6 +61,19 @@ export class MonacoQuickOpenService extends QuickOpenService {
const widget = this.widget;
widget.show(this.opts.prefix || '');
widget.setPlaceHolder(opts.inputAriaLabel);
if (opts.password) {
widget.setPassword(opts.password);
} else {
widget.setPassword(false);
}
}

clearInputDecoration(): void {
this.widget.clearInputDecoration();
}

showInputDecoration(severity: monaco.Severity): void {
this.widget.showInputDecoration(severity);
}

protected get widget(): monaco.quickOpen.QuickOpenWidget {
Expand All @@ -78,7 +93,7 @@ export class MonacoQuickOpenService extends QuickOpenService {
this.onClose(true);
},
onType: lookFor => this.onType(lookFor || ''),
onFocusLost: () => false
onFocusLost: () => (this.opts && this.opts.ignoreFocusOut !== undefined) ? this.opts.ignoreFocusOut : false
}, {});
this.attachQuickOpenStyler();
this._widget.create();
Expand Down
4 changes: 4 additions & 0 deletions packages/monaco/src/typings/monaco/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ declare module monaco.quickOpen {
layout(dimension: monaco.editor.IDimension): void;
show(prefix: string, options?: IShowOptions): void;
hide(reason?: HideReason): void;
refresh(input?: IModel<any>, autoFocus?: IAutoFocus): void;
setPassword(isPassword: boolean): void;
showInputDecoration(decoration: Severity): void;
clearInputDecoration(): void;
}

export enum HideReason {
Expand Down
10 changes: 5 additions & 5 deletions packages/plugin-ext/src/api/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ export interface StatusBarMessageRegistryMain {

export interface QuickOpenExt {
$onItemSelected(handle: number): void;
$validateInput(input: string): PromiseLike<string> | undefined;
$validateInput(input: string): PromiseLike<string | undefined> | undefined;
}

export interface QuickOpenMain {
$show(options: PickOptions): PromiseLike<number | number[]>;
$setItems(items: PickOpenItem[]): PromiseLike<any>;
$setError(error: Error): PromiseLike<any>;
$input(options: theia.InputBoxOptions, validateInput: boolean): PromiseLike<string>;
$show(options: PickOptions): Promise<number | number[]>;
$setItems(items: PickOpenItem[]): Promise<any>;
$setError(error: Error): Promise<any>;
$input(options: theia.InputBoxOptions, validateInput: boolean): Promise<string | undefined>;
}

export interface WindowStateExt {
Expand Down
24 changes: 16 additions & 8 deletions packages/plugin-ext/src/main/browser/quick-open-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,27 @@

import { InputBoxOptions } from '@theia/plugin';
import { interfaces } from 'inversify';
import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-service';
import { QuickOpenModel, QuickOpenItem, QuickOpenMode } from '@theia/core/lib/browser/quick-open/quick-open-model';
import { RPCProtocol } from '../../api/rpc-protocol';
import { QuickOpenExt, QuickOpenMain, MAIN_RPC_CONTEXT, PickOptions, PickOpenItem } from '../../api/plugin-api';
import { MonacoQuickOpenService } from '@theia/monaco/lib/browser/monaco-quick-open-service';
import { QuickInputService } from '@theia/monaco/lib/browser/monaco-quick-input-service';

export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel {

private quickInput: QuickInputService;
private doResolve: (value?: number | number[] | PromiseLike<number | number[]> | undefined) => void;
private proxy: QuickOpenExt;
private delegate: QuickOpenService;
private delegate: MonacoQuickOpenService;
private acceptor: ((items: QuickOpenItem[]) => void) | undefined;
private items: QuickOpenItem[] | undefined;

private activeElement: HTMLElement | undefined;

constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT);
this.delegate = container.get(QuickOpenService);
this.delegate = container.get(MonacoQuickOpenService);
this.quickInput = container.get(QuickInputService);
}

private cleanUp() {
Expand All @@ -43,7 +46,7 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel {
this.activeElement = undefined;
}

$show(options: PickOptions): PromiseLike<number | number[]> {
$show(options: PickOptions): Promise<number | number[]> {
this.activeElement = window.document.activeElement as HTMLElement;
this.delegate.open(this, {
fuzzyMatchDescription: options.matchOnDescription,
Expand All @@ -61,7 +64,7 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel {

}
// tslint:disable-next-line:no-any
$setItems(items: PickOpenItem[]): PromiseLike<any> {
$setItems(items: PickOpenItem[]): Promise<any> {
this.items = [];
for (const i of items) {
this.items.push(new QuickOpenItem({
Expand All @@ -85,11 +88,16 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel {
return Promise.resolve();
}
// tslint:disable-next-line:no-any
$setError(error: Error): PromiseLike<any> {
$setError(error: Error): Promise<any> {
throw new Error("Method not implemented.");
}
$input(options: InputBoxOptions, validateInput: boolean): PromiseLike<string> {
throw new Error("Method not implemented.");

$input(options: InputBoxOptions, validateInput: boolean): Promise<string | undefined> {
if (validateInput) {
options.validateInput = val => this.proxy.$validateInput(val);
}

return this.quickInput.open(options);
}

onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ export function createAPI(rpc: RPCProtocol): typeof theia {
setStatusBarMessage(text: string, arg?: number | PromiseLike<any>): Disposable {
return statusBarMessageRegistryExt.setStatusBarMessage(text, arg);
},
showInputBox(options?: theia.InputBoxOptions, token?: theia.CancellationToken) {
if (token) {
const coreEvent = Object.assign(token.onCancellationRequested, { maxListeners: 0 });
const coreCancellationToken = { isCancellationRequested: token.isCancellationRequested, onCancellationRequested: coreEvent };
return quickOpenExt.showInput(options, coreCancellationToken);
} else {
return quickOpenExt.showInput(options);
}
},
createStatusBarItem(alignment?: theia.StatusBarAlignment, priority?: number): theia.StatusBarItem {
return statusBarMessageRegistryExt.createStatusBarItem(alignment, priority);
},
Expand Down
13 changes: 10 additions & 3 deletions packages/plugin-ext/src/plugin/quick-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* 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 } from '@theia/plugin';
import { QuickPickOptions, QuickPickItem, InputBoxOptions } from '@theia/plugin';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { RPCProtocol } from '../api/rpc-protocol';
import { ExtendedPromise } from '../api/extended-promise';
Expand All @@ -25,7 +25,7 @@ export type Item = string | QuickPickItem;
export class QuickOpenExtImpl implements QuickOpenExt {
private proxy: QuickOpenMain;
private selectItemHandler: undefined | ((handle: number) => void);
private validateInputHandler: undefined | ((input: string) => string | PromiseLike<string>);
private validateInputHandler: undefined | ((input: string) => string | PromiseLike<string | undefined> | undefined);

constructor(rpc: RPCProtocol) {
this.proxy = rpc.getProxy(Ext.QUICK_OPEN_MAIN);
Expand All @@ -35,7 +35,7 @@ export class QuickOpenExtImpl implements QuickOpenExt {
this.selectItemHandler(handle);
}
}
$validateInput(input: string): PromiseLike<string> | undefined {
$validateInput(input: string): PromiseLike<string | undefined> | undefined {
if (this.validateInputHandler) {
return Promise.resolve(this.validateInputHandler(input));
}
Expand Down Expand Up @@ -108,4 +108,11 @@ export class QuickOpenExtImpl implements QuickOpenExt {
});
return hookCancellationToken<Item | Item[] | undefined>(token, promise);
}

showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): PromiseLike<string | undefined> {
this.validateInputHandler = options && options.validateInput;

const promise = this.proxy.$input(options!, typeof this.validateInputHandler === 'function');
return hookCancellationToken(token, promise);
}
}
Loading

0 comments on commit 77141d3

Please sign in to comment.