Skip to content

Commit 05cb255

Browse files
authored
Create environment proposed API (#21074)
1 parent a009edb commit 05cb255

12 files changed

+270
-109
lines changed

src/client/environmentApi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ResolvedEnvironment,
3232
Resource,
3333
} from './apiTypes';
34+
import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi';
3435

3536
type ActiveEnvironmentChangeEvent = {
3637
resource: WorkspaceFolder | undefined;
@@ -253,6 +254,7 @@ export function buildEnvironmentApi(
253254
sendApiTelemetry('onDidChangeEnvironments');
254255
return onEnvironmentsChanged.event;
255256
},
257+
...buildEnvironmentCreationApi(),
256258
};
257259
return environmentApi;
258260
}

src/client/proposedApiTypes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
export interface ProposedExtensionAPI {}
4+
export interface ProposedExtensionAPI {
5+
/**
6+
* Top level proposed APIs should go here.
7+
*/
8+
}

src/client/pythonEnvironments/creation/createEnvApi.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import { IInterpreterQuickPick } from '../../interpreter/configuration/types';
99
import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment';
1010
import { condaCreationProvider } from './provider/condaCreationProvider';
1111
import { VenvCreationProvider } from './provider/venvCreationProvider';
12+
import { showInformationMessage } from '../../common/vscodeApis/windowApis';
13+
import { CreateEnv } from '../../common/utils/localize';
1214
import {
13-
CreateEnvironmentExitedEventArgs,
14-
CreateEnvironmentOptions,
1515
CreateEnvironmentProvider,
16+
CreateEnvironmentOptions,
1617
CreateEnvironmentResult,
17-
} from './types';
18-
import { showInformationMessage } from '../../common/vscodeApis/windowApis';
19-
import { CreateEnv } from '../../common/utils/localize';
18+
ProposedCreateEnvironmentAPI,
19+
EnvironmentDidCreateEvent,
20+
} from './proposed.createEnvApis';
2021

2122
class CreateEnvironmentProviders {
2223
private _createEnvProviders: CreateEnvironmentProvider[] = [];
@@ -26,6 +27,9 @@ class CreateEnvironmentProviders {
2627
}
2728

2829
public add(provider: CreateEnvironmentProvider) {
30+
if (this._createEnvProviders.filter((p) => p.id === provider.id).length > 0) {
31+
throw new Error(`Create Environment provider with id ${provider.id} already registered`);
32+
}
2933
this._createEnvProviders.push(provider);
3034
}
3135

@@ -63,15 +67,36 @@ export function registerCreateEnvironmentFeatures(
6367
return handleCreateEnvironmentCommand(providers, options);
6468
},
6569
),
66-
);
67-
disposables.push(registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)));
68-
disposables.push(registerCreateEnvironmentProvider(condaCreationProvider()));
69-
disposables.push(
70-
onCreateEnvironmentExited(async (e: CreateEnvironmentExitedEventArgs) => {
71-
if (e.result?.path && e.options?.selectEnvironment) {
72-
await interpreterPathService.update(e.result.uri, ConfigurationTarget.WorkspaceFolder, e.result.path);
73-
showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.result.path)}`);
70+
registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)),
71+
registerCreateEnvironmentProvider(condaCreationProvider()),
72+
onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => {
73+
if (e.path && e.options?.selectEnvironment) {
74+
await interpreterPathService.update(
75+
e.workspaceFolder?.uri,
76+
ConfigurationTarget.WorkspaceFolder,
77+
e.path,
78+
);
79+
showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`);
7480
}
7581
}),
7682
);
7783
}
84+
85+
export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI {
86+
return {
87+
onWillCreateEnvironment: onCreateEnvironmentStarted,
88+
onDidCreateEnvironment: onCreateEnvironmentExited,
89+
createEnvironment: async (
90+
options?: CreateEnvironmentOptions | undefined,
91+
): Promise<CreateEnvironmentResult | undefined> => {
92+
const providers = _createEnvironmentProviders.getAll();
93+
try {
94+
return await handleCreateEnvironmentCommand(providers, options);
95+
} catch (err) {
96+
return { path: undefined, workspaceFolder: undefined, action: undefined, error: err as Error };
97+
}
98+
},
99+
registerCreateEnvironmentProvider: (provider: CreateEnvironmentProvider) =>
100+
registerCreateEnvironmentProvider(provider),
101+
};
102+
}

src/client/pythonEnvironments/creation/createEnvironment.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import {
1111
} from '../../common/vscodeApis/windowApis';
1212
import { traceError, traceVerbose } from '../../logging';
1313
import {
14-
CreateEnvironmentExitedEventArgs,
1514
CreateEnvironmentOptions,
16-
CreateEnvironmentProvider,
1715
CreateEnvironmentResult,
18-
CreateEnvironmentStartedEventArgs,
19-
} from './types';
16+
CreateEnvironmentProvider,
17+
EnvironmentWillCreateEvent,
18+
EnvironmentDidCreateEvent,
19+
} from './proposed.createEnvApis';
2020

21-
const onCreateEnvironmentStartedEvent = new EventEmitter<CreateEnvironmentStartedEventArgs>();
22-
const onCreateEnvironmentExitedEvent = new EventEmitter<CreateEnvironmentExitedEventArgs>();
21+
const onCreateEnvironmentStartedEvent = new EventEmitter<EnvironmentWillCreateEvent>();
22+
const onCreateEnvironmentExitedEvent = new EventEmitter<EnvironmentDidCreateEvent>();
2323

2424
let startedEventCount = 0;
2525

@@ -32,14 +32,20 @@ function fireStartedEvent(options?: CreateEnvironmentOptions): void {
3232
startedEventCount += 1;
3333
}
3434

35-
function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: unknown): void {
36-
onCreateEnvironmentExitedEvent.fire({ result, options, error });
35+
function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void {
36+
onCreateEnvironmentExitedEvent.fire({
37+
options,
38+
workspaceFolder: result?.workspaceFolder,
39+
path: result?.path,
40+
action: result?.action,
41+
error: error || result?.error,
42+
});
3743
startedEventCount -= 1;
3844
}
3945

4046
export function getCreationEvents(): {
41-
onCreateEnvironmentStarted: Event<CreateEnvironmentStartedEventArgs>;
42-
onCreateEnvironmentExited: Event<CreateEnvironmentExitedEventArgs>;
47+
onCreateEnvironmentStarted: Event<EnvironmentWillCreateEvent>;
48+
onCreateEnvironmentExited: Event<EnvironmentDidCreateEvent>;
4349
isCreatingEnvironment: () => boolean;
4450
} {
4551
return {
@@ -54,7 +60,7 @@ async function createEnvironment(
5460
options: CreateEnvironmentOptions,
5561
): Promise<CreateEnvironmentResult | undefined> {
5662
let result: CreateEnvironmentResult | undefined;
57-
let err: unknown | undefined;
63+
let err: Error | undefined;
5864
try {
5965
fireStartedEvent(options);
6066
result = await provider.createEnvironment(options);
@@ -65,7 +71,7 @@ async function createEnvironment(
6571
return undefined;
6672
}
6773
}
68-
err = ex;
74+
err = ex as Error;
6975
throw err;
7076
} finally {
7177
fireExitedEvent(result, options, err);
@@ -185,11 +191,7 @@ export async function handleCreateEnvironmentCommand(
185191
const action = await MultiStepNode.run(envTypeStep);
186192
if (options?.showBackButton) {
187193
if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) {
188-
result = {
189-
path: result?.path,
190-
uri: result?.uri,
191-
action: action === MultiStepAction.Back ? 'Back' : 'Cancel',
192-
};
194+
result = { action, workspaceFolder: undefined, path: undefined, error: undefined };
193195
}
194196
}
195197

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License
3+
4+
import { Event, Disposable, WorkspaceFolder } from 'vscode';
5+
import { EnvironmentTools } from '../../apiTypes';
6+
7+
export type CreateEnvironmentUserActions = 'Back' | 'Cancel';
8+
export type EnvironmentProviderId = string;
9+
10+
/**
11+
* Options used when creating a Python environment.
12+
*/
13+
export interface CreateEnvironmentOptions {
14+
/**
15+
* Default `true`. If `true`, the environment creation handler is expected to install packages.
16+
*/
17+
installPackages?: boolean;
18+
19+
/**
20+
* Default `true`. If `true`, the environment creation provider is expected to add the environment to ignore list
21+
* for the source control.
22+
*/
23+
ignoreSourceControl?: boolean;
24+
25+
/**
26+
* Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput.
27+
*/
28+
showBackButton?: boolean;
29+
30+
/**
31+
* Default `true`. If `true`, the environment after creation will be selected.
32+
*/
33+
selectEnvironment?: boolean;
34+
}
35+
36+
/**
37+
* Params passed on `onWillCreateEnvironment` event handler.
38+
*/
39+
export interface EnvironmentWillCreateEvent {
40+
/**
41+
* Options used to create a Python environment.
42+
*/
43+
options: CreateEnvironmentOptions | undefined;
44+
}
45+
46+
/**
47+
* Params passed on `onDidCreateEnvironment` event handler.
48+
*/
49+
export interface EnvironmentDidCreateEvent extends CreateEnvironmentResult {
50+
/**
51+
* Options used to create the Python environment.
52+
*/
53+
options: CreateEnvironmentOptions | undefined;
54+
}
55+
56+
export interface CreateEnvironmentResult {
57+
/**
58+
* Workspace folder associated with the environment.
59+
*/
60+
workspaceFolder: WorkspaceFolder | undefined;
61+
62+
/**
63+
* Path to the executable python in the environment
64+
*/
65+
path: string | undefined;
66+
67+
/**
68+
* User action that resulted in exit from the create environment flow.
69+
*/
70+
action: CreateEnvironmentUserActions | undefined;
71+
72+
/**
73+
* Error if any occurred during environment creation.
74+
*/
75+
error: Error | undefined;
76+
}
77+
78+
/**
79+
* Extensions that want to contribute their own environment creation can do that by registering an object
80+
* that implements this interface.
81+
*/
82+
export interface CreateEnvironmentProvider {
83+
/**
84+
* This API is called when user selects this provider from a QuickPick to select the type of environment
85+
* user wants. This API is expected to show a QuickPick or QuickInput to get the user input and return
86+
* the path to the Python executable in the environment.
87+
*
88+
* @param {CreateEnvironmentOptions} [options] Options used to create a Python environment.
89+
*
90+
* @returns a promise that resolves to the path to the
91+
* Python executable in the environment. Or any action taken by the user, such as back or cancel.
92+
*/
93+
createEnvironment(options?: CreateEnvironmentOptions): Promise<CreateEnvironmentResult | undefined>;
94+
95+
/**
96+
* Unique ID for the creation provider, typically <ExtensionId>:<environment-type | guid>
97+
*/
98+
id: EnvironmentProviderId;
99+
100+
/**
101+
* Display name for the creation provider.
102+
*/
103+
name: string;
104+
105+
/**
106+
* Description displayed to the user in the QuickPick to select environment provider.
107+
*/
108+
description: string;
109+
110+
/**
111+
* Tools used to manage this environment. e.g., ['conda']. In the most to least priority order
112+
* for resolving and working with the environment.
113+
*/
114+
tools: EnvironmentTools[];
115+
}
116+
117+
export interface ProposedCreateEnvironmentAPI {
118+
/**
119+
* This API can be used to detect when the environment creation starts for any registered
120+
* provider (including internal providers). This will also receive any options passed in
121+
* or defaults used to create environment.
122+
*/
123+
onWillCreateEnvironment: Event<EnvironmentWillCreateEvent>;
124+
125+
/**
126+
* This API can be used to detect when the environment provider exits for any registered
127+
* provider (including internal providers). This will also receive created environment path,
128+
* any errors, or user actions taken from the provider.
129+
*/
130+
onDidCreateEnvironment: Event<EnvironmentDidCreateEvent>;
131+
132+
/**
133+
* This API will show a QuickPick to select an environment provider from available list of
134+
* providers. Based on the selection the `createEnvironment` will be called on the provider.
135+
*/
136+
createEnvironment(options?: CreateEnvironmentOptions): Promise<CreateEnvironmentResult | undefined>;
137+
138+
/**
139+
* This API should be called to register an environment creation provider. It returns
140+
* a (@link Disposable} which can be used to remove the registration.
141+
*/
142+
registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable;
143+
}

src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import { CancellationToken, ProgressLocation, WorkspaceFolder } from 'vscode';
55
import * as path from 'path';
66
import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants';
77
import { traceError, traceLog } from '../../../logging';
8-
import {
9-
CreateEnvironmentOptions,
10-
CreateEnvironmentProgress,
11-
CreateEnvironmentProvider,
12-
CreateEnvironmentResult,
13-
} from '../types';
8+
import { CreateEnvironmentProgress } from '../types';
149
import { pickWorkspaceFolder } from '../common/workspaceSelection';
1510
import { execObservable } from '../../../common/process/rawProcessApis';
1611
import { createDeferred } from '../../../common/utils/async';
@@ -28,6 +23,11 @@ import {
2823
CONDA_ENV_EXISTING_MARKER,
2924
} from './condaProgressAndTelemetry';
3025
import { splitLines } from '../../../common/stringUtils';
26+
import {
27+
CreateEnvironmentOptions,
28+
CreateEnvironmentResult,
29+
CreateEnvironmentProvider,
30+
} from '../proposed.createEnvApis';
3131

3232
function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] {
3333
let addGitIgnore = true;
@@ -247,7 +247,7 @@ async function createEnvironment(options?: CreateEnvironmentOptions): Promise<Cr
247247
showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment);
248248
}
249249
}
250-
return { path: envPath, uri: workspace?.uri };
250+
return { path: envPath, workspaceFolder: workspace, action: undefined, error: undefined };
251251
},
252252
);
253253
}
@@ -260,5 +260,7 @@ export function condaCreationProvider(): CreateEnvironmentProvider {
260260
description: CreateEnv.Conda.providerDescription,
261261

262262
id: `${PVSC_EXTENSION_ID}:conda`,
263+
264+
tools: ['Conda'],
263265
};
264266
}

0 commit comments

Comments
 (0)