Skip to content

Commit 995b0bc

Browse files
authored
Add support for 'back' to all create env UI. (#20693)
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.
1 parent f3ecbf5 commit 995b0bc

File tree

18 files changed

+902
-284
lines changed

18 files changed

+902
-284
lines changed

pythonFiles/create_venv.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,14 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
154154
if pip_installed:
155155
upgrade_pip(venv_path)
156156

157-
if args.requirements:
158-
print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}")
159-
install_requirements(venv_path, args.requirements)
160-
161157
if args.toml:
162158
print(f"VENV_INSTALLING_PYPROJECT: {args.toml}")
163159
install_toml(venv_path, args.extras)
164160

161+
if args.requirements:
162+
print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}")
163+
install_requirements(venv_path, args.requirements)
164+
165165

166166
if __name__ == "__main__":
167167
main(sys.argv[1:])

src/client/common/utils/async.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface Deferred<T> {
2727
readonly rejected: boolean;
2828
readonly completed: boolean;
2929
resolve(value?: T | PromiseLike<T>): void;
30-
reject(reason?: string | Error | Record<string, unknown>): void;
30+
reject(reason?: string | Error | Record<string, unknown> | unknown): void;
3131
}
3232

3333
class DeferredImpl<T> implements Deferred<T> {

src/client/common/utils/multiStepInput.ts

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { inject, injectable } from 'inversify';
99
import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPick, QuickPickItem, Event } from 'vscode';
1010
import { IApplicationShell } from '../application/types';
11+
import { createDeferred } from './async';
1112

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

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

32-
type QuickInputButtonSetup = {
33+
export type QuickInputButtonSetup = {
3334
/**
3435
* Button for an action in a QuickPick.
3536
*/
@@ -164,35 +165,41 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
164165
// so do it after initialization. This ensures quickpick starts with the active
165166
// item in focus when this is true, instead of having scroll position at top.
166167
input.keepScrollPosition = keepScrollPosition;
167-
try {
168-
return await new Promise<MultiStepInputQuickPicResponseType<T, P>>((resolve, reject) => {
169-
disposables.push(
170-
input.onDidTriggerButton(async (item) => {
171-
if (item === QuickInputButtons.Back) {
172-
reject(InputFlowAction.back);
173-
}
174-
if (customButtonSetups) {
175-
for (const customButtonSetup of customButtonSetups) {
176-
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
177-
await customButtonSetup?.callback(input);
178-
}
179-
}
168+
169+
const deferred = createDeferred<T>();
170+
171+
disposables.push(
172+
input.onDidTriggerButton(async (item) => {
173+
if (item === QuickInputButtons.Back) {
174+
deferred.reject(InputFlowAction.back);
175+
input.hide();
176+
}
177+
if (customButtonSetups) {
178+
for (const customButtonSetup of customButtonSetups) {
179+
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
180+
await customButtonSetup?.callback(input);
180181
}
181-
}),
182-
input.onDidChangeSelection((selectedItems) => resolve(selectedItems[0])),
183-
input.onDidHide(() => {
184-
resolve(undefined);
185-
}),
186-
);
187-
if (acceptFilterBoxTextAsSelection) {
188-
disposables.push(
189-
input.onDidAccept(() => {
190-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
191-
resolve(input.value as any);
192-
}),
193-
);
182+
}
194183
}
195-
});
184+
}),
185+
input.onDidChangeSelection((selectedItems) => deferred.resolve(selectedItems[0])),
186+
input.onDidHide(() => {
187+
if (!deferred.completed) {
188+
deferred.resolve(undefined);
189+
}
190+
}),
191+
);
192+
if (acceptFilterBoxTextAsSelection) {
193+
disposables.push(
194+
input.onDidAccept(() => {
195+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
196+
deferred.resolve(input.value as any);
197+
}),
198+
);
199+
}
200+
201+
try {
202+
return await deferred.promise;
196203
} finally {
197204
disposables.forEach((d) => d.dispose());
198205
}
@@ -277,6 +284,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
277284
if (err === InputFlowAction.back) {
278285
this.steps.pop();
279286
step = this.steps.pop();
287+
if (step === undefined) {
288+
throw err;
289+
}
280290
} else if (err === InputFlowAction.resume) {
281291
step = this.steps.pop();
282292
} else if (err === InputFlowAction.cancel) {

src/client/common/vscodeApis/windowApis.ts

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
3+
/* eslint-disable @typescript-eslint/no-explicit-any */
4+
/* eslint-disable max-classes-per-file */
35

46
import {
57
CancellationToken,
68
MessageItem,
79
MessageOptions,
810
Progress,
911
ProgressOptions,
12+
QuickPick,
13+
QuickInputButtons,
1014
QuickPickItem,
1115
QuickPickOptions,
1216
TextEditor,
1317
window,
18+
Disposable,
1419
} from 'vscode';
20+
import { createDeferred, Deferred } from '../utils/async';
1521

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

30+
export function createQuickPick<T extends QuickPickItem>(): QuickPick<T> {
31+
return window.createQuickPick<T>();
32+
}
33+
2534
export function showErrorMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>;
2635
export function showErrorMessage<T extends string>(
2736
message: string,
@@ -67,3 +76,122 @@ export function getActiveTextEditor(): TextEditor | undefined {
6776
const { activeTextEditor } = window;
6877
return activeTextEditor;
6978
}
79+
80+
export enum MultiStepAction {
81+
Back = 'Back',
82+
Cancel = 'Cancel',
83+
Continue = 'Continue',
84+
}
85+
86+
export async function showQuickPickWithBack<T extends QuickPickItem>(
87+
items: readonly T[],
88+
options?: QuickPickOptions,
89+
token?: CancellationToken,
90+
): Promise<T | T[] | undefined> {
91+
const quickPick: QuickPick<T> = window.createQuickPick<T>();
92+
const disposables: Disposable[] = [quickPick];
93+
94+
quickPick.items = items;
95+
quickPick.buttons = [QuickInputButtons.Back];
96+
quickPick.canSelectMany = options?.canPickMany ?? false;
97+
quickPick.ignoreFocusOut = options?.ignoreFocusOut ?? false;
98+
quickPick.matchOnDescription = options?.matchOnDescription ?? false;
99+
quickPick.matchOnDetail = options?.matchOnDetail ?? false;
100+
quickPick.placeholder = options?.placeHolder;
101+
quickPick.title = options?.title;
102+
103+
const deferred = createDeferred<T | T[] | undefined>();
104+
105+
disposables.push(
106+
quickPick,
107+
quickPick.onDidTriggerButton((item) => {
108+
if (item === QuickInputButtons.Back) {
109+
deferred.reject(MultiStepAction.Back);
110+
quickPick.hide();
111+
}
112+
}),
113+
quickPick.onDidAccept(() => {
114+
if (!deferred.completed) {
115+
deferred.resolve(quickPick.selectedItems.map((item) => item));
116+
quickPick.hide();
117+
}
118+
}),
119+
quickPick.onDidHide(() => {
120+
if (!deferred.completed) {
121+
deferred.resolve(undefined);
122+
}
123+
}),
124+
);
125+
if (token) {
126+
disposables.push(
127+
token.onCancellationRequested(() => {
128+
quickPick.hide();
129+
}),
130+
);
131+
}
132+
quickPick.show();
133+
134+
try {
135+
return await deferred.promise;
136+
} finally {
137+
disposables.forEach((d) => d.dispose());
138+
}
139+
}
140+
141+
export class MultiStepNode {
142+
constructor(
143+
public previous: MultiStepNode | undefined,
144+
public readonly current: (context?: MultiStepAction) => Promise<MultiStepAction>,
145+
public next: MultiStepNode | undefined,
146+
) {}
147+
148+
public static async run(step: MultiStepNode, context?: MultiStepAction): Promise<MultiStepAction> {
149+
let nextStep: MultiStepNode | undefined = step;
150+
let flowAction = await nextStep.current(context);
151+
while (nextStep !== undefined) {
152+
if (flowAction === MultiStepAction.Cancel) {
153+
return flowAction;
154+
}
155+
if (flowAction === MultiStepAction.Back) {
156+
nextStep = nextStep?.previous;
157+
}
158+
if (flowAction === MultiStepAction.Continue) {
159+
nextStep = nextStep?.next;
160+
}
161+
162+
if (nextStep) {
163+
flowAction = await nextStep?.current(flowAction);
164+
}
165+
}
166+
167+
return flowAction;
168+
}
169+
}
170+
171+
export function createStepBackEndNode<T>(deferred?: Deferred<T>): MultiStepNode {
172+
return new MultiStepNode(
173+
undefined,
174+
async () => {
175+
if (deferred) {
176+
// This is to ensure we don't leave behind any pending promises.
177+
deferred.reject(MultiStepAction.Back);
178+
}
179+
return Promise.resolve(MultiStepAction.Back);
180+
},
181+
undefined,
182+
);
183+
}
184+
185+
export function createStepForwardEndNode<T>(deferred?: Deferred<T>, result?: T): MultiStepNode {
186+
return new MultiStepNode(
187+
undefined,
188+
async () => {
189+
if (deferred) {
190+
// This is to ensure we don't leave behind any pending promises.
191+
deferred.resolve(result);
192+
}
193+
return Promise.resolve(MultiStepAction.Back);
194+
},
195+
undefined,
196+
);
197+
}

0 commit comments

Comments
 (0)