Skip to content

Commit

Permalink
#2153 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 22, 2018
1 parent 48a4697 commit de47925
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 16 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 @@ -35,6 +35,7 @@ decorate(injectable(), ProtocolToMonacoConverter);
import '../../src/browser/style/index.css';
import '../../src/browser/style/symbol-sprite.svg';
import '../../src/browser/style/symbol-icons.css';
import { MonacoQuickInputService } from './monaco-quick-input-service';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoToProtocolConverter).toSelf().inSingletonScope();
Expand Down Expand Up @@ -76,4 +77,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(QuickOpenService).toDynamicValue(ctx =>
ctx.container.get(MonacoQuickOpenService)
).inSingletonScope();

bind(MonacoQuickInputService).toSelf().inSingletonScope();
});
163 changes: 163 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,163 @@
/*
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
import { MonacoQuickOpenService } from "./monaco-quick-open-service";
import { injectable, inject } from "inversify";
import { ILogger } from "@theia/core/lib/common/logger";
import { QuickOpenItem, QuickOpenItemOptions, QuickOpenMode } from "@theia/core/lib/browser/quick-open";

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>;
}

@injectable()
export class MonacoQuickInputService extends MonacoQuickOpenService {
private static promptMessage = "Press 'Enter' to confirm your input or 'Escape' to cancel";
private options: QuickInputOptions;
constructor(@inject(ILogger) logger: ILogger) {
super(logger);

}

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

const inputItem = new InputOpenItemOptions(this.options.prompt);
this.open({
onType: (s, a) => this.validateInput(s, a, inputItem)
}, {
prefix: options.value,
placeholder: options.placeHolder,
onClose: () => inputItem.resolve(undefined)
});
if (options.password) {
this.widget.setPassword(true);
}

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

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

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

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

protected get widget(): monaco.quickOpen.QuickOpenWidget {
if (this._widget) {
return this._widget;
}
this._widget = new monaco.quickOpen.QuickOpenWidget(this.container, {
onOk: () => {
this.previousActiveElement = undefined;
this.onClose(false);
},
onCancel: () => {
if (this.previousActiveElement instanceof HTMLElement) {
this.previousActiveElement.focus();
}
this.previousActiveElement = undefined;
this.onClose(true);
},
onType: lookFor => this.onType(lookFor || ''),
onFocusLost: () => this.options.ignoreFocusOut !== undefined ? this.options.ignoreFocusOut : false
}, {});
this.attachQuickOpenStyler();
this._widget.create();
return this._widget;
}
}

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;
}
}
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 @@ -377,6 +377,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 @@ -88,14 +88,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
18 changes: 13 additions & 5 deletions packages/plugin-ext/src/main/browser/quick-open-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { QuickOpenService } from '@theia/core/lib/browser/quick-open/quick-open-
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 { MonacoQuickInputService } from '@theia/monaco/lib/browser/monaco-quick-input-service';

export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel {

Expand All @@ -21,10 +22,12 @@ export class QuickOpenMainImpl implements QuickOpenMain, QuickOpenModel {
private items: QuickOpenItem[] | undefined;

private activeElement: HTMLElement | undefined;
private input: MonacoQuickInputService;

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

private cleanUp() {
Expand All @@ -34,7 +37,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 @@ -52,7 +55,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 @@ -76,11 +79,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.input.input(options);
}

onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void {
Expand Down
11 changes: 10 additions & 1 deletion packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,20 @@ 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);
},
createOutputChannel(name: string): theia.OutputChannel {
return outputChannelRegistryExt.createOutputChannel(name);
return outputChannelRegistryExt.createOutputChannel(name);
},

get state(): theia.WindowState {
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 @@ -5,7 +5,7 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-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 @@ -16,7 +16,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 @@ -26,7 +26,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 @@ -99,4 +99,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);
}
}
19 changes: 19 additions & 0 deletions packages/plugin/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ theia.window.showQuickPick(["foo", "bar", "foobar"], option).then((val: string[]
});
```

#### Input Box

Function to ask user for input.

Example of using:

```typescript
const option: theia.InputBoxOptions = {
prompt:"Hello from Plugin",
placeHolder:"Type text there",
ignoreFocusOut: false,
password: false,
value:"Default value"
};
theia.window.showInputBox(option).then((s: string | undefined) => {
console.log(typeof s !== 'undefined'? s : "Input was canceled");
});
```

#### Notification API

A notification shows an information message to users.
Expand Down
Loading

0 comments on commit de47925

Please sign in to comment.