diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 51afbdc951b8..30ba5d84cf5f 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -7,6 +7,7 @@ import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; import { Commands as LSCommands } from '../../activation/commands'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../tensorBoard/constants'; import { Channel, Commands, CommandSource } from '../constants'; +import { CreateEnvironmentOptions } from '../../pythonEnvironments/creation/proposed.createEnvApis'; export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; @@ -56,6 +57,7 @@ export type AllCommands = keyof ICommandNameArgumentTypeMapping; * @extends {ICommandNameWithoutArgumentTypeMapping} */ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping { + [Commands.Create_Environment]: [CreateEnvironmentOptions]; ['vscode.openWith']: [Uri, string]; ['workbench.action.quickOpen']: [string]; ['workbench.action.openWalkthrough']: [string | { category: string; step: string }, boolean | undefined]; diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index ac9387784c2c..0eaade703371 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -74,12 +74,14 @@ export namespace Octicons { export const Test_Skip = '$(circle-slash)'; export const Downloading = '$(cloud-download)'; export const Installing = '$(desktop-download)'; + export const Search = '$(search)'; export const Search_Stop = '$(search-stop)'; export const Star = '$(star-full)'; export const Gear = '$(gear)'; export const Warning = '$(warning)'; export const Error = '$(error)'; export const Lightbulb = '$(lightbulb)'; + export const Folder = '$(folder)'; } /** diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f09573614108..b997e168ce3e 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -259,6 +259,9 @@ export namespace InterpreterQuickPickList { }; export const refreshInterpreterList = l10n.t('Refresh Interpreter list'); export const refreshingInterpreterList = l10n.t('Refreshing Interpreter list...'); + export const create = { + label: l10n.t('Create Virtual Environment...'), + }; } export namespace OutputChannelNames { diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 9b8ecec74f9f..cf0712e160c7 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -81,8 +81,13 @@ export namespace EnvGroups { @injectable() export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implements IInterpreterQuickPick { + private readonly createEnvironmentSuggestion: QuickPickItem = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, + alwaysShow: true, + }; + private readonly manualEntrySuggestion: ISpecialQuickPickItem = { - label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, alwaysShow: true, }; @@ -220,6 +225,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem } else if (selection.label === this.manualEntrySuggestion.label) { sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_OR_FIND); return this._enterOrBrowseInterpreterPath.bind(this); + } else if (selection.label === this.createEnvironmentSuggestion.label) { + this.commandManager + .executeCommand(Commands.Create_Environment, { + showBackButton: false, + selectEnvironment: true, + }) + .then(noop, noop); } else if (selection.label === this.noPythonInstalled.label) { this.commandManager.executeCommand(Commands.InstallPython).then(noop, noop); this.wasNoPythonInstalledItemClicked = true; @@ -237,7 +249,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem filter: ((i: PythonEnvironment) => boolean) | undefined, params?: InterpreterQuickPickParams, ): QuickPickType[] { - const suggestions: QuickPickType[] = [this.manualEntrySuggestion]; + const suggestions: QuickPickType[] = []; + if (params?.showCreateEnvironment) { + suggestions.push(this.createEnvironmentSuggestion, { label: '', kind: QuickPickItemKind.Separator }); + } + + suggestions.push(this.manualEntrySuggestion, { label: '', kind: QuickPickItemKind.Separator }); + const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource); if (defaultInterpreterPathSuggestion) { suggestions.push(defaultInterpreterPathSuggestion); @@ -553,7 +571,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem const wkspace = targetConfig[0].folderUri; const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this._pickInterpreter(input, s, undefined), interpreterState); + await multiStep.run( + (input, s) => this._pickInterpreter(input, s, undefined, { showCreateEnvironment: true }), + interpreterState, + ); if (interpreterState.path !== undefined) { // User may choose to have an empty string stored, so variable `interpreterState.path` may be diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 2f3882e1246e..815de29045d3 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -80,6 +80,11 @@ export interface InterpreterQuickPickParams { * Specify `true` to show back button. */ showBackButton?: boolean; + + /** + * Show button to create a new environment. + */ + showCreateEnvironment?: boolean; } export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts index f177db5c2a32..e1b3d42400fa 100644 --- a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -155,7 +155,11 @@ suite('Set Interpreter Command', () => { } as PythonEnvironment, }; const expectedEnterInterpreterPathSuggestion = { - label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`, + label: `${Octicons.Folder} ${InterpreterQuickPickList.enterPath.label}`, + alwaysShow: true, + }; + const expectedCreateEnvSuggestion = { + label: `${Octicons.Add} ${InterpreterQuickPickList.create.label}`, alwaysShow: true, }; const currentPythonPath = 'python'; @@ -237,6 +241,7 @@ suite('Set Interpreter Command', () => { recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -278,11 +283,66 @@ suite('Set Interpreter Command', () => { assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); }); + test('Picker should show create env when set in options', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType>(); + const recommended = cloneDeep(item); + recommended.label = `${Octicons.Star} ${item.label}`; + recommended.description = interpreterPath; + const suggestions = [ + expectedCreateEnvSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, + recommended, + ]; + const expectedParameters: IQuickPickParameters = { + placeholder: `Selected Interpreter: ${currentPythonPath}`, + items: suggestions, + matchOnDetail: true, + matchOnDescription: true, + title: InterpreterQuickPickList.browsePath.openButtonLabel, + sortByLabel: true, + keepScrollPosition: true, + }; + let actualParameters: IQuickPickParameters | undefined; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .callback((options) => { + actualParameters = options; + }) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state, undefined, { + showCreateEnvironment: true, + }); + + expect(actualParameters).to.not.equal(undefined, 'Parameters not set'); + const refreshButtons = actualParameters!.customButtonSetups; + expect(refreshButtons).to.not.equal(undefined, 'Callback not set'); + delete actualParameters!.initialize; + delete actualParameters!.customButtonSetups; + delete actualParameters!.onChangeItem; + if (typeof actualParameters!.activeItem === 'function') { + const activeItem = await actualParameters!.activeItem(({ items: suggestions } as unknown) as QuickPick< + QuickPickType + >); + assert.deepStrictEqual(activeItem, recommended); + } else { + assert(false, 'Not a function'); + } + delete actualParameters!.activeItem; + assert.deepStrictEqual(actualParameters, expectedParameters, 'Params not equal'); + }); + test('Picker should be displayed with expected items if no interpreters are available', async () => { const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; const multiStepInput = TypeMoq.Mock.ofType>(); const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, noPythonInstalled, ]; @@ -440,6 +500,7 @@ suite('Set Interpreter Command', () => { recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -556,6 +617,7 @@ suite('Set Interpreter Command', () => { recommended.description = interpreterPath; const suggestions = [ expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, defaultInterpreterPathSuggestion, { kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended }, recommended, @@ -652,7 +714,13 @@ suite('Set Interpreter Command', () => { alwaysShow: true, }; - const suggestions = [expectedEnterInterpreterPathSuggestion, defaultPathSuggestion, separator, recommended]; + const suggestions = [ + expectedEnterInterpreterPathSuggestion, + { kind: QuickPickItemKind.Separator, label: '' }, + defaultPathSuggestion, + separator, + recommended, + ]; const expectedParameters: IQuickPickParameters = { placeholder: `Selected Interpreter: ${currentPythonPath}`, items: suggestions,