// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPickItem, window, } from "vscode"; export type InputStep = ( input: MultiStepInput, state: T, ) => Promise | void>; export class InputFlowAction { public static back = new InputFlowAction(); public static cancel = new InputFlowAction(); public static resume = new InputFlowAction(); } export interface IQuickPickParameters { title?: string; step?: number; totalSteps?: number; canGoBack?: boolean; items: ReadonlyArray; activeItem?: T; placeholder: string; buttons?: QuickInputButton[]; matchOnDescription?: boolean; matchOnDetail?: boolean; acceptFilterBoxTextAsSelection?: boolean; shouldResume?(): Promise; } export interface InputBoxParameters { title: string; password?: boolean; step?: number; totalSteps?: number; value: string; prompt: string; buttons?: QuickInputButton[]; validate(value: string): Promise; shouldResume?(): Promise; } type MultiStepInputQuickPicResponseType = | T | (P extends { buttons: (infer I)[] } ? I : never); type MultiStepInputInputBoxResponseType

= | string | (P extends { buttons: (infer I)[] } ? I : never); export interface IMultiStepInput { run(start: InputStep, state: S): Promise; showQuickPick>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume, }: P): Promise>; showInputBox

({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume, }: P): Promise>; } export class MultiStepInput implements IMultiStepInput { private current?: QuickInput; private steps: InputStep[] = []; public run(start: InputStep, state: S): Promise { return this.stepThrough(start, state); } public async showQuickPick>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume, matchOnDescription, matchOnDetail, acceptFilterBoxTextAsSelection, }: P): Promise> { const disposables: Disposable[] = []; try { return await new Promise>( (resolve, reject) => { const input = window.createQuickPick(); input.title = title; input.step = step; input.totalSteps = totalSteps; input.placeholder = placeholder; input.ignoreFocusOut = true; input.items = items; input.matchOnDescription = matchOnDescription || false; input.matchOnDetail = matchOnDetail || false; if (activeItem) { input.activeItems = [activeItem]; } else { input.activeItems = []; } input.buttons = [ ...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || []), ]; disposables.push( input.onDidTriggerButton(item => { if (item === QuickInputButtons.Back) { reject(InputFlowAction.back); } else { resolve(item); } }), input.onDidChangeSelection(selectedItems => resolve(selectedItems[0])), input.onDidHide(() => { (async () => { reject( shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel, ); })().catch(reject); }), ); if (acceptFilterBoxTextAsSelection) { disposables.push( input.onDidAccept(() => { resolve(input.value); }), ); } if (this.current) { this.current.dispose(); } this.current = input; this.current.show(); }, ); } finally { disposables.forEach(d => d.dispose()); } } public async showInputBox

({ title, step, totalSteps, value, prompt, validate, password, buttons, shouldResume, }: P): Promise> { const disposables: Disposable[] = []; try { return await new Promise>((resolve, reject) => { const input = window.createInputBox(); input.title = title; input.step = step; input.totalSteps = totalSteps; input.password = !!password; input.value = value || ""; input.prompt = prompt; input.ignoreFocusOut = true; input.buttons = [ ...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || []), ]; let validating = validate(""); disposables.push( input.onDidTriggerButton(item => { if (item === QuickInputButtons.Back) { reject(InputFlowAction.back); } else { resolve(item); } }), input.onDidAccept(async () => { const inputValue = input.value; input.enabled = false; input.busy = true; if (!(await validate(inputValue))) { resolve(inputValue); } input.enabled = true; input.busy = false; }), input.onDidChangeValue(async text => { const current = validate(text); validating = current; const validationMessage = await current; if (current === validating) { input.validationMessage = validationMessage; } }), input.onDidHide(() => { (async () => { reject( shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel, ); })().catch(reject); }), ); if (this.current) { this.current.dispose(); } this.current = input; this.current.show(); }); } finally { disposables.forEach(d => d.dispose()); } } private async stepThrough(start: InputStep, state: S) { 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, state); } 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(); } } }