From 6064df241034fe5486898ae557fe776ac746984b Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 24 May 2018 16:45:23 +0200 Subject: [PATCH] QuickInput API --- .../src/quickInputExploration.ts | 249 ++++++++++++++++++ .../extension-editing/src/typings/ref.d.ts | 1 + src/vs/vscode.proposed.d.ts | 80 +++++- 3 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 extensions/extension-editing/src/quickInputExploration.ts diff --git a/extensions/extension-editing/src/quickInputExploration.ts b/extensions/extension-editing/src/quickInputExploration.ts new file mode 100644 index 0000000000000..7cf36bd89bb3a --- /dev/null +++ b/extensions/extension-editing/src/quickInputExploration.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext, commands, QuickPickItem, window, Disposable, CancellationToken, QuickInputCommand, QuickInput } from 'vscode'; + +export function activate(context: ExtensionContext) { + context.subscriptions.push(commands.registerCommand('foobar', async () => { + const inputs = await collectInputs(); + console.log(inputs); + })); +} + +const resourceGroups: QuickPickItem[] = ['vscode-data-function', 'vscode-website-microservices', 'vscode-website-monitor', 'vscode-website-preview', 'vscode-website-prod'] + .map(label => ({ label })); + + +interface Result { + resourceGroup: QuickPickItem | string; + name: string; + runtime: QuickPickItem; +} + +async function collectInputs() { + const result = {} as Partial; + await MultiStepInput.run(input => pickResourceGroup(input, {})); + return result; +} + +class MyCommand implements QuickInputCommand { + constructor(public iconPath: string) { } +} + +async function pickResourceGroup(input: MultiStepInput, state: Partial) { + const createResourceGroupItem = new MyCommand('createResourceGroup.svg'); + const pick = await input.showQuickPick({ + placeHolder: 'Pick a resource group', + items: resourceGroups, + commands: [createResourceGroupItem], + shouldResume: shouldResume + }); + if (pick instanceof MyCommand) { + return (input: MultiStepInput) => inputResourceGroupName(input, state); + } + state.resourceGroup = pick; + return (input: MultiStepInput) => inputName(input, state); +} + +async function inputResourceGroupName(input: MultiStepInput, state: Partial) { + state.resourceGroup = await input.showInputBox({ + prompt: 'Choose a unique name for the resource group', + validate: validateNameIsUnique, + shouldResume: shouldResume + }); + return (input: MultiStepInput) => inputName(input, state); +} + +async function inputName(input: MultiStepInput, state: Partial) { + state.name = await input.showInputBox({ + prompt: 'Choose a unique name for the application service', + validate: validateNameIsUnique, + shouldResume: shouldResume + }); + return (input: MultiStepInput) => pickRuntime(input, state); +} + +async function pickRuntime(input: MultiStepInput, state: Partial) { + const runtimes = await getAvailableRuntimes(state.resourceGroup, null /* token */); + state.runtime = await input.showQuickPick({ + placeHolder: 'Pick a runtime', + items: runtimes, + shouldResume: shouldResume + }); +} + +function shouldResume() { + // Could show a notification with the option to resume. + return new Promise((resolve, reject) => { + + }); +} + +class InputFlowAction { + private constructor() { } + static back = new InputFlowAction(); + static cancel = new InputFlowAction(); + static resume = new InputFlowAction(); +} + +type InputStep = (input: MultiStepInput) => Thenable; + +interface QuickPickParameters { + items: QuickPickItem[]; + placeHolder: string; + commands?: QuickInputCommand[]; + shouldResume: () => Thenable; +} + +interface InputBoxParameters { + prompt: string; + validate: (value: string) => Promise; + commands?: QuickInputCommand[]; + shouldResume: () => Thenable; +} + +const backItem: QuickInputCommand = { iconPath: 'back.svg' }; + +class MultiStepInput { + + static async run(start: InputStep) { + const input = new MultiStepInput(); + return input.stepThrough(start); + } + + private current?: QuickInput; + private steps: InputStep[] = []; + + private async stepThrough(start: InputStep) { + let step: InputStep | void = start; + while (step) { + this.steps.push(step); + if (this.current) { + this.current.enabled = false; + this.current.busy = true; + } + try { + step = await step(this); + } catch (err) { + if (err === InputFlowAction.back) { + this.steps.pop(); + step = this.steps.pop(); + } else if (err === InputFlowAction.resume) { + step = this.steps.pop(); + } else if (err === InputFlowAction.cancel) { + step = undefined; + } else { + throw err; + } + } + } + if (this.current) { + this.current.dispose(); + } + } + + async showQuickPick

({ items, placeHolder, commands, shouldResume }: P) { + const disposables: Disposable[] = []; + try { + return await new Promise((resolve, reject) => { + const input = window.createQuickPick(); + input.placeholder = placeHolder; + input.items = items; + input.commands = [ + ...(this.steps.length > 1 ? [backItem] : []), + ...(commands || []) + ]; + disposables.push( + input, + input.onDidTriggerCommand(item => { + if (item === backItem) { + reject(InputFlowAction.back); + } + }), + input.onDidSelectItem(item => resolve(item)), + input.onHide(() => { + (async () => { + reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); + })() + .catch(reject); + }) + ); + if (this.current) { + this.current.hide(); + } + this.current = input; + this.current.show(); + }); + } finally { + disposables.forEach(d => d.dispose()); + } + } + + async showInputBox

({ prompt, validate, commands, shouldResume }: P) { + const disposables: Disposable[] = []; + try { + return await new Promise((resolve, reject) => { + const input = window.createInputBox(); + input.prompt = prompt; + input.commands = [ + ...(this.steps.length > 1 ? [backItem] : []), + ...(commands || []) + ]; + let validating = validate(''); + disposables.push( + input, + input.onDidTriggerCommand(item => { + if (item === backItem) { + reject(InputFlowAction.back); + } + }), + input.onDidAccept(async text => { + if (!(await validate(text))) { + resolve(text); + } + }), + input.onDidValueChange(async text => { + const current = validate(text); + validating = current; + const validationMessage = await current; + if (current === validating) { + input.validationMessage = validationMessage; + } + }), + input.onHide(() => { + (async () => { + reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); + })() + .catch(reject); + }) + ); + if (this.current) { + this.current.hide(); + } + this.current = input; + this.current.show(); + }); + } finally { + disposables.forEach(d => d.dispose()); + } + } +} + + +// --------------------------------------------------------------------------------------- + + +async function validateNameIsUnique(name: string) { + // ...validate... + await new Promise(resolve => setTimeout(resolve, 1000)); + return name === 'vscode' ? 'Name not unique' : undefined; +} + +async function getAvailableRuntimes(resourceGroup: QuickPickItem | string, token: CancellationToken): Promise { + // ...retrieve... + await new Promise(resolve => setTimeout(resolve, 2000)); + return ['Node 8.9', 'Node 6.11', 'Node 4.5'] + .map(label => ({ label })); +} diff --git a/extensions/extension-editing/src/typings/ref.d.ts b/extensions/extension-editing/src/typings/ref.d.ts index 216911a680eb2..c9849d48e083f 100644 --- a/extensions/extension-editing/src/typings/ref.d.ts +++ b/extensions/extension-editing/src/typings/ref.d.ts @@ -4,3 +4,4 @@ *--------------------------------------------------------------------------------------------*/ /// +/// diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b7c2346400e7e..6bd99a8a26e99 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -5,6 +5,8 @@ // This is the place for API experiments and proposal. +import { QuickPickItem } from 'vscode'; + declare module 'vscode' { export namespace window { @@ -601,22 +603,74 @@ declare module 'vscode' { export namespace window { - /** - * Collect multiple inputs from the user. The provided handler will be called with a - * [`QuickInput`](#QuickInput) that should be used to control the UI. - * - * @param handler The callback that will collect the inputs. - */ - export function multiStepInput(handler: (input: QuickInput, token: CancellationToken) => Thenable, token?: CancellationToken): Thenable; + export function createQuickPick(): QuickPick; + export function createInputBox(): InputBox; } - /** - * Controls the UI within a multi-step input session. The handler passed to [`window.multiStepInput`](#window.multiStepInput) - * should use the instance of this interface passed to it to collect all inputs. - */ export interface QuickInput { - showQuickPick: typeof window.showQuickPick; - showInputBox: typeof window.showInputBox; + + enabled: boolean; + + busy: boolean; + + show(): void; + + hide(): void; + + onHide: Event; + + dispose(): void; + } + + export interface QuickPick extends QuickInput { + + value: string; + + placeholder: string; + + onDidValueChange: Event; + + onDidAccept: Event; + + commands: QuickInputCommand[]; + + onDidTriggerCommand: Event; + + items: QuickPickItem[]; + + canSelectMany: boolean; + + builtInFilter: boolean; + + selectedItems: QuickPickItem[]; + + onDidSelectItem: Event; + } + + export interface InputBox extends QuickInput { + + value: string; + + placeholder: string; + + password: boolean; + + onDidValueChange: Event; + + onDidAccept: Event; + + commands: QuickInputCommand[]; + + onDidTriggerCommand: Event; + + prompt: string; + + validationMessage: string; + } + + export interface QuickInputCommand { + iconPath: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; + tooltip?: string | undefined; } //#endregion