Skip to content

Commit

Permalink
Add support for 'back' to all create env UI. (#20693)
Browse files Browse the repository at this point in the history
Closes #20274


### Usage

This change allows callers of the Create Environment command to handle
`Back` and `Cancel`:
``` typescript
let result: CreateEnvironmentResult | undefined;
try {
    const result = await commands.executeCommand("python.createEnvironment", {showBackButton: true});
} catch(e) {
   // error while creating environment
}

if (result?.action === 'Back') {
    // user clicked Back
}

if (result?.action === 'Cancel') {
    // user pressed escape or Cancel
}
```
I decided to go with `result?.action` because we don't have a npm
package for python extension API so catching particular exception might
be error prone with `ex instanceof <error>`. We will provide a proper
interface via `api.environments` for create environment, and
contribution to create environment. Until that point this command will
provide the stop gap.

### Notes

1. I did not use the multi-step input that is used in the rest of the
extension because, the existing implementation does not have context.
Consider the following scenario: venv -> workspace select -> python
select -> packages. Assume that there is only one workspace, and we
don't show the workspace selection UI, that decision is done inside the
workspace step. So, if there is only 1 workspace it is a short circuit
to next step. User is on python selection and clicks `back`, workspace
selection short circuits to next step which is python selection. So,
from user perspective, back does not work. This can be fixed by sending
context that the reason control moved to previous step was because user
clicked on back.
2. This makes a change to old multi step API to rethrow the exception,
if user hits `back` and the current step has no steps to go back to.
  • Loading branch information
karthiknadig committed Feb 14, 2023
1 parent f3ecbf5 commit 995b0bc
Show file tree
Hide file tree
Showing 18 changed files with 902 additions and 284 deletions.
8 changes: 4 additions & 4 deletions pythonFiles/create_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
if pip_installed:
upgrade_pip(venv_path)

if args.requirements:
print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}")
install_requirements(venv_path, args.requirements)

if args.toml:
print(f"VENV_INSTALLING_PYPROJECT: {args.toml}")
install_toml(venv_path, args.extras)

if args.requirements:
print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}")
install_requirements(venv_path, args.requirements)


if __name__ == "__main__":
main(sys.argv[1:])
2 changes: 1 addition & 1 deletion src/client/common/utils/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface Deferred<T> {
readonly rejected: boolean;
readonly completed: boolean;
resolve(value?: T | PromiseLike<T>): void;
reject(reason?: string | Error | Record<string, unknown>): void;
reject(reason?: string | Error | Record<string, unknown> | unknown): void;
}

class DeferredImpl<T> implements Deferred<T> {
Expand Down
66 changes: 38 additions & 28 deletions src/client/common/utils/multiStepInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { inject, injectable } from 'inversify';
import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPick, QuickPickItem, Event } from 'vscode';
import { IApplicationShell } from '../application/types';
import { createDeferred } from './async';

// Borrowed from https://github.com/Microsoft/vscode-extension-samples/blob/master/quickinput-sample/src/multiStepInput.ts
// Why re-invent the wheel :)
Expand All @@ -29,7 +30,7 @@ export type InputStep<T extends any> = (input: MultiStepInput<T>, state: T) => P

type buttonCallbackType<T extends QuickPickItem> = (quickPick: QuickPick<T>) => void;

type QuickInputButtonSetup = {
export type QuickInputButtonSetup = {
/**
* Button for an action in a QuickPick.
*/
Expand Down Expand Up @@ -164,35 +165,41 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
// so do it after initialization. This ensures quickpick starts with the active
// item in focus when this is true, instead of having scroll position at top.
input.keepScrollPosition = keepScrollPosition;
try {
return await new Promise<MultiStepInputQuickPicResponseType<T, P>>((resolve, reject) => {
disposables.push(
input.onDidTriggerButton(async (item) => {
if (item === QuickInputButtons.Back) {
reject(InputFlowAction.back);
}
if (customButtonSetups) {
for (const customButtonSetup of customButtonSetups) {
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
await customButtonSetup?.callback(input);
}
}

const deferred = createDeferred<T>();

disposables.push(
input.onDidTriggerButton(async (item) => {
if (item === QuickInputButtons.Back) {
deferred.reject(InputFlowAction.back);
input.hide();
}
if (customButtonSetups) {
for (const customButtonSetup of customButtonSetups) {
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
await customButtonSetup?.callback(input);
}
}),
input.onDidChangeSelection((selectedItems) => resolve(selectedItems[0])),
input.onDidHide(() => {
resolve(undefined);
}),
);
if (acceptFilterBoxTextAsSelection) {
disposables.push(
input.onDidAccept(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve(input.value as any);
}),
);
}
}
});
}),
input.onDidChangeSelection((selectedItems) => deferred.resolve(selectedItems[0])),
input.onDidHide(() => {
if (!deferred.completed) {
deferred.resolve(undefined);
}
}),
);
if (acceptFilterBoxTextAsSelection) {
disposables.push(
input.onDidAccept(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deferred.resolve(input.value as any);
}),
);
}

try {
return await deferred.promise;
} finally {
disposables.forEach((d) => d.dispose());
}
Expand Down Expand Up @@ -277,6 +284,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
if (err === InputFlowAction.back) {
this.steps.pop();
step = this.steps.pop();
if (step === undefined) {
throw err;
}
} else if (err === InputFlowAction.resume) {
step = this.steps.pop();
} else if (err === InputFlowAction.cancel) {
Expand Down
130 changes: 129 additions & 1 deletion src/client/common/vscodeApis/windowApis.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */

import {
CancellationToken,
MessageItem,
MessageOptions,
Progress,
ProgressOptions,
QuickPick,
QuickInputButtons,
QuickPickItem,
QuickPickOptions,
TextEditor,
window,
Disposable,
} from 'vscode';
import { createDeferred, Deferred } from '../utils/async';

/* eslint-disable @typescript-eslint/no-explicit-any */
export function showQuickPick<T extends QuickPickItem>(
items: readonly T[] | Thenable<readonly T[]>,
options?: QuickPickOptions,
Expand All @@ -22,6 +27,10 @@ export function showQuickPick<T extends QuickPickItem>(
return window.showQuickPick(items, options, token);
}

export function createQuickPick<T extends QuickPickItem>(): QuickPick<T> {
return window.createQuickPick<T>();
}

export function showErrorMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>;
export function showErrorMessage<T extends string>(
message: string,
Expand Down Expand Up @@ -67,3 +76,122 @@ export function getActiveTextEditor(): TextEditor | undefined {
const { activeTextEditor } = window;
return activeTextEditor;
}

export enum MultiStepAction {
Back = 'Back',
Cancel = 'Cancel',
Continue = 'Continue',
}

export async function showQuickPickWithBack<T extends QuickPickItem>(
items: readonly T[],
options?: QuickPickOptions,
token?: CancellationToken,
): Promise<T | T[] | undefined> {
const quickPick: QuickPick<T> = window.createQuickPick<T>();
const disposables: Disposable[] = [quickPick];

quickPick.items = items;
quickPick.buttons = [QuickInputButtons.Back];
quickPick.canSelectMany = options?.canPickMany ?? false;
quickPick.ignoreFocusOut = options?.ignoreFocusOut ?? false;
quickPick.matchOnDescription = options?.matchOnDescription ?? false;
quickPick.matchOnDetail = options?.matchOnDetail ?? false;
quickPick.placeholder = options?.placeHolder;
quickPick.title = options?.title;

const deferred = createDeferred<T | T[] | undefined>();

disposables.push(
quickPick,
quickPick.onDidTriggerButton((item) => {
if (item === QuickInputButtons.Back) {
deferred.reject(MultiStepAction.Back);
quickPick.hide();
}
}),
quickPick.onDidAccept(() => {
if (!deferred.completed) {
deferred.resolve(quickPick.selectedItems.map((item) => item));
quickPick.hide();
}
}),
quickPick.onDidHide(() => {
if (!deferred.completed) {
deferred.resolve(undefined);
}
}),
);
if (token) {
disposables.push(
token.onCancellationRequested(() => {
quickPick.hide();
}),
);
}
quickPick.show();

try {
return await deferred.promise;
} finally {
disposables.forEach((d) => d.dispose());
}
}

export class MultiStepNode {
constructor(
public previous: MultiStepNode | undefined,
public readonly current: (context?: MultiStepAction) => Promise<MultiStepAction>,
public next: MultiStepNode | undefined,
) {}

public static async run(step: MultiStepNode, context?: MultiStepAction): Promise<MultiStepAction> {
let nextStep: MultiStepNode | undefined = step;
let flowAction = await nextStep.current(context);
while (nextStep !== undefined) {
if (flowAction === MultiStepAction.Cancel) {
return flowAction;
}
if (flowAction === MultiStepAction.Back) {
nextStep = nextStep?.previous;
}
if (flowAction === MultiStepAction.Continue) {
nextStep = nextStep?.next;
}

if (nextStep) {
flowAction = await nextStep?.current(flowAction);
}
}

return flowAction;
}
}

export function createStepBackEndNode<T>(deferred?: Deferred<T>): MultiStepNode {
return new MultiStepNode(
undefined,
async () => {
if (deferred) {
// This is to ensure we don't leave behind any pending promises.
deferred.reject(MultiStepAction.Back);
}
return Promise.resolve(MultiStepAction.Back);
},
undefined,
);
}

export function createStepForwardEndNode<T>(deferred?: Deferred<T>, result?: T): MultiStepNode {
return new MultiStepNode(
undefined,
async () => {
if (deferred) {
// This is to ensure we don't leave behind any pending promises.
deferred.resolve(result);
}
return Promise.resolve(MultiStepAction.Back);
},
undefined,
);
}
Loading

0 comments on commit 995b0bc

Please sign in to comment.