Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preselect dropdown values in the New Project Wizard #2730

Merged
merged 9 commits into from
Apr 11, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
*--------------------------------------------------------------------------------------------*/

.dropdown-entry {
display: grid;
grid-template-columns: [icon] min-content [left] max-content [middle] 1fr [right] max-content [end];
align-items: center;
}

.dropdown-entry > .dropdown-entry-icon {
grid-column: icon / left;
margin-right: 0.2em;
}

.dropdown-entry > .dropdown-entry-title {
grid-column: left / middle;
}

.dropdown-entry > .dropdown-entry-subtitle {
grid-column: middle / right;
font-size: 0.9em;
opacity: 0.7;
margin-left: 0.6em;
}

.dropdown-entry > .dropdown-entry-group {
grid-column: right / end;
opacity: 0.8;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
*--------------------------------------------------------------------------------------------*/

// CSS.
import 'vs/css!./dropdownEntry';

// React.
import * as React from 'react';

/**
* DropdownEntryProps interface.
*/
interface DropdownEntryProps {
icon?: string;
title: string;
subtitle: string;
group?: string;
}

/**
* DropdownEntry component.
* @param props The dropdown entry props.
* @returns The rendered component
*/
export const DropdownEntry = (props: DropdownEntryProps) => {
// Render.
return (
<div className='dropdown-entry'>
{props.icon ? <div className='dropdown-entry-icon'>{props.icon}</div> : null}
<div className='dropdown-entry-title'>
{props.title}
</div>
<div className='dropdown-entry-subtitle'>
{props.subtitle}
</div>
{props.group ? <div className='dropdown-entry-group'>{props.group}</div> : null}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { PropsWithChildren } from 'react'; // eslint-disable-line no-duplicate-
import { localize } from 'vs/nls';
import { useNewProjectWizardContext } from 'vs/workbench/browser/positronNewProjectWizard/newProjectWizardContext';
import { URI } from 'vs/base/common/uri';
import { NewProjectWizardStep } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardEnums';
import { NewProjectType, NewProjectWizardStep } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardEnums';
import { NewProjectWizardStepProps } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardStepProps';
import { NewProjectType } from 'vs/workbench/browser/positronNewProjectWizard/newProjectWizardState';
import { PositronWizardStep } from 'vs/workbench/browser/positronNewProjectWizard/components/wizardStep';
import { PositronWizardSubStep } from 'vs/workbench/browser/positronNewProjectWizard/components/wizardSubStep';
import { LabeledTextInput } from 'vs/workbench/browser/positronComponents/positronModalDialog/components/labeledTextInput';
Expand Down Expand Up @@ -106,7 +105,7 @@ export const ProjectNameLocationStep = (props: PropsWithChildren<NewProjectWizar
'projectNameLocationSubStep.parentDirectory.description',
'Select a directory to create your project in'
))()}
value={projectConfig.parentFolder} // this should be <code>formatted
value={projectConfig.parentFolder} // TODO: this should be <code>formatted
onBrowse={browseHandler}
onChange={e => setProjectConfig({ ...projectConfig, parentFolder: e.target.value })}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import { PropsWithChildren } from 'react'; // eslint-disable-line no-duplicate-
import { Button } from 'vs/base/browser/ui/positronComponents/button/button';
import { localize } from 'vs/nls';
import { useNewProjectWizardContext } from 'vs/workbench/browser/positronNewProjectWizard/newProjectWizardContext';
import { NewProjectWizardStep } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardEnums';
import { NewProjectType, NewProjectWizardStep } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardEnums';
import { NewProjectWizardStepProps } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardStepProps';
import { NewProjectType } from 'vs/workbench/browser/positronNewProjectWizard/newProjectWizardState';
import { OKCancelBackNextActionBar } from 'vs/workbench/browser/positronComponents/positronModalDialog/components/okCancelBackNextActionBar';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import { NewProjectWizardStepProps } from 'vs/workbench/browser/positronNewProje
import { localize } from 'vs/nls';
import { RuntimeStartupPhase } from 'vs/workbench/services/runtimeStartup/common/runtimeStartupService';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { createCondaInterpreterDropDownItems, createPythonInterpreterDropDownItems, createVenvInterpreterDropDownItems } from 'vs/workbench/browser/positronNewProjectWizard/utilities/pythonInterpreterListUtils';
import { getEnvTypeEntries, getPythonInterpreterEntries, getSelectedPythonInterpreterId, locationForNewEnv } from 'vs/workbench/browser/positronNewProjectWizard/utilities/pythonEnvironmentStepUtils';
import { PositronWizardStep } from 'vs/workbench/browser/positronNewProjectWizard/components/wizardStep';
import { PositronWizardSubStep } from 'vs/workbench/browser/positronNewProjectWizard/components/wizardSubStep';
import { DropDownListBoxItem } from 'vs/workbench/browser/positronComponents/dropDownListBox/dropDownListBoxItem';
import { DropDownListBox } from 'vs/workbench/browser/positronComponents/dropDownListBox/dropDownListBox';
import { RadioButtonItem } from 'vs/workbench/browser/positronComponents/positronModalDialog/components/radioButton';
import { RadioGroup } from 'vs/workbench/browser/positronComponents/positronModalDialog/components/radioGroup';
import { EnvironmentSetupType } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardEnums';
import { EnvironmentSetupType, PythonEnvironmentType } from 'vs/workbench/browser/positronNewProjectWizard/interfaces/newProjectWizardEnums';
import { PythonInterpreterEntry } from 'vs/workbench/browser/positronNewProjectWizard/components/steps/pythonInterpreterEntry';
import { DropdownEntry } from 'vs/workbench/browser/positronNewProjectWizard/components/steps/dropdownEntry';

/**
* The PythonEnvironmentStep component is specific to Python projects in the new project wizard.
Expand All @@ -39,28 +39,29 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
// Hooks to manage the startup phase and interpreter entries.
const [startupPhase, setStartupPhase] =
useState(newProjectWizardState.runtimeStartupService.startupPhase);
const [envSetupType, setEnvSetupType] = useState(projectConfig.pythonEnvSetupType);
const [envType, setEnvType] = useState(projectConfig.pythonEnvType);
const [selectedInterpreter, setSelectedInterpreter] = useState<string | undefined>(
getSelectedPythonInterpreterId(
projectConfig.selectedRuntime?.runtimeId,
newProjectWizardState.runtimeStartupService
)
);
const [interpreterEntries, setInterpreterEntries] =
useState(
// It's possible that the runtime discovery phase is not complete, so we need to check
// for that before creating the interpreter entries.
startupPhase !== RuntimeStartupPhase.Complete ?
[] :
// TODO: we currently populate the interpreter entries with all registered runtimes,
// but we'll want to call the Venv or Conda interpreter creation functions based on
// the default selection.
createPythonInterpreterDropDownItems(
getPythonInterpreterEntries(
newProjectWizardState.runtimeStartupService,
newProjectWizardState.languageRuntimeService
newProjectWizardState.languageRuntimeService,
envSetupType,
envType
)
);
const [envSetupType, setEnvSetupType] = useState(EnvironmentSetupType.NewEnvironment);

// TODO: retrieve the python environment types from the language runtime service somehow?
// TODO: localize these entries
const envTypeEntries = [
new DropDownListBoxItem({ identifier: 'Venv', title: 'Venv' + ' Creates a `.venv` virtual environment for your project', value: 'Venv' }),
new DropDownListBoxItem({ identifier: 'Conda', title: 'Conda' + ' Creates a `.conda` Conda environment for your project', value: 'Conda' })
];
const envTypeEntries = getEnvTypeEntries();

const envSetupRadioButtons: RadioButtonItem[] = [
new RadioButtonItem({ identifier: EnvironmentSetupType.NewEnvironment, title: 'Create a new Python environment _(Recommended)_' }),
Expand All @@ -70,46 +71,52 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
// Handler for when the environment setup type is selected. If the user selects the "existing
// environment" setup, the env type dropdown will not show and the interpreter entries will be
// updated to show all existing interpreters.
const onEnvSetupSelected = (identifier: string) => {
// Verify that the identifier is a valid EnvironmentSetupType value
if (Object.values(EnvironmentSetupType).includes(identifier as EnvironmentSetupType)) {
setEnvSetupType(identifier as EnvironmentSetupType);
// If the user selects an existing environment, update the interpreter entries dropdown
// to show the unfiltered list of all existing interpreters.
if (identifier === EnvironmentSetupType.ExistingEnvironment) {
setInterpreterEntries(
createPythonInterpreterDropDownItems(
newProjectWizardState.runtimeStartupService,
newProjectWizardState.languageRuntimeService
)
);
}
} else {
// This shouldn't happen, since the RadioGroup should only allow selection of the
// EnvironmentSetupType values
logService.error(`Unknown environment setup type: ${identifier}`);
}
const onEnvSetupSelected = (pythonEnvSetupType: EnvironmentSetupType) => {
setEnvSetupType(pythonEnvSetupType);
// If the user selects an existing environment, update the interpreter entries dropdown
// to show the unfiltered list of all existing interpreters.
setInterpreterEntries(
getPythonInterpreterEntries(
newProjectWizardState.runtimeStartupService,
newProjectWizardState.languageRuntimeService,
pythonEnvSetupType,
envType
)
);
setSelectedInterpreter(
getSelectedPythonInterpreterId(
projectConfig.selectedRuntime?.runtimeId,
newProjectWizardState.runtimeStartupService
)
);
setProjectConfig({ ...projectConfig, pythonEnvSetupType });
};

// Handler for when the environment type is selected. The interpreter entries are updated based
// on the selected environment type, and the project configuration is updated as well.
const onEnvTypeSelected = (identifier: string) => {
switch (identifier) {
case 'Venv':
setInterpreterEntries(createVenvInterpreterDropDownItems(newProjectWizardState.runtimeStartupService, newProjectWizardState.languageRuntimeService));
break;
case 'Conda':
setInterpreterEntries(createCondaInterpreterDropDownItems());
break;
default:
logService.error(`Unknown environment type: ${identifier}`);
}
setProjectConfig({ ...projectConfig, pythonEnvType: identifier });
const onEnvTypeSelected = (pythonEnvType: PythonEnvironmentType) => {
setEnvType(pythonEnvType);
setInterpreterEntries(
getPythonInterpreterEntries(
newProjectWizardState.runtimeStartupService,
newProjectWizardState.languageRuntimeService,
envSetupType,
pythonEnvType
)
);
setSelectedInterpreter(
getSelectedPythonInterpreterId(
projectConfig.selectedRuntime?.runtimeId,
newProjectWizardState.runtimeStartupService
)
);
setProjectConfig({ ...projectConfig, pythonEnvType });
};

// Handler for when the interpreter is selected. The project configuration is updated with the
// selected interpreter.
const onInterpreterSelected = (identifier: string) => {
setSelectedInterpreter(identifier);
const selectedRuntime = newProjectWizardState.languageRuntimeService.getRegisteredRuntime(identifier);
if (!selectedRuntime) {
// This shouldn't happen, since the DropDownListBox should only allow selection of registered
Expand All @@ -133,12 +140,17 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
newProjectWizardState.runtimeStartupService.onDidChangeRuntimeStartupPhase(
phase => {
if (phase === RuntimeStartupPhase.Complete) {
// TODO: instead of calling createPythonInterpreterComboBoxItems, it should
// be aware of the defaults set by the environment type (Venv, Conda)
setInterpreterEntries(
createPythonInterpreterDropDownItems(
newProjectWizardState.runtimeStartupService,
newProjectWizardState.languageRuntimeService
const entries = getPythonInterpreterEntries(
newProjectWizardState.runtimeStartupService,
newProjectWizardState.languageRuntimeService,
envSetupType,
envType
);
setInterpreterEntries(entries);
setSelectedInterpreter(
getSelectedPythonInterpreterId(
projectConfig.selectedRuntime?.runtimeId,
newProjectWizardState.runtimeStartupService
)
);
}
Expand Down Expand Up @@ -178,8 +190,8 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
name='envSetup'
labelledBy='pythonEnvironment-howToSetUpEnv'
entries={envSetupRadioButtons}
initialSelectionId={EnvironmentSetupType.NewEnvironment}
onSelectionChanged={identifier => onEnvSetupSelected(identifier)}
initialSelectionId={projectConfig.pythonEnvSetupType}
onSelectionChanged={identifier => onEnvSetupSelected(identifier as EnvironmentSetupType)}
/>
</PositronWizardSubStep>
{envSetupType === EnvironmentSetupType.NewEnvironment ?
Expand All @@ -192,15 +204,13 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
'pythonEnvironmentSubStep.description',
'Select an environment type for your project.'
))()}
// TODO: construct the env location based on the envTypeEntries above, instead of inline here
feedback={(() => localize(
'pythonEnvironmentSubStep.feedback',
'The {0} environment will be created at: {1}',
projectConfig.pythonEnvType,
`${projectConfig.parentFolder}/${projectConfig.projectName}/${projectConfig.pythonEnvType === 'Venv' ? '.venv' : 'Conda' ? '.conda' : ''}`
envType,
locationForNewEnv(projectConfig.parentFolder, projectConfig.projectName, envType)
))()}
>
{/* TODO: how to pre-select an option? */}
<DropDownListBox
keybindingService={keybindingService}
layoutService={layoutService}
Expand All @@ -209,7 +219,9 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
'Select an environment type'
))()}
entries={envTypeEntries}
onSelectionChanged={dropDownListBoxItem => onEnvTypeSelected(dropDownListBoxItem.options.identifier)}
selectedIdentifier={envType}
createItem={item => <DropdownEntry title={item.options.value.envType} subtitle={item.options.value.envDescription} />}
onSelectionChanged={item => onEnvTypeSelected(item.options.identifier)}
/>
</PositronWizardSubStep> : null
}
Expand Down Expand Up @@ -244,11 +256,12 @@ export const PythonEnvironmentStep = (props: PropsWithChildren<NewProjectWizardS
// interpreters, show a message that no suitable interpreters were found and the
// user should install an interpreter with minimum version
entries={startupPhase !== RuntimeStartupPhase.Complete ? [] : interpreterEntries}
createItem={dropDownListBoxItem =>
<PythonInterpreterEntry pythonInterpreterInfo={dropDownListBoxItem.options.value} />
selectedIdentifier={selectedInterpreter}
createItem={item =>
<PythonInterpreterEntry pythonInterpreterInfo={item.options.value} />
}
onSelectionChanged={dropDownListBoxItem =>
onInterpreterSelected(dropDownListBoxItem.options.identifier)
onSelectionChanged={item =>
onInterpreterSelected(item.options.identifier)
}
/>
</PositronWizardSubStep>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import 'vs/css!./pythonInterpreterEntry';
import * as React from 'react';

// Other dependencies.
import { PythonInterpreterInfo } from 'vs/workbench/browser/positronNewProjectWizard/utilities/pythonInterpreterListUtils';
import { PythonInterpreterInfo } from 'vs/workbench/browser/positronNewProjectWizard/utilities/pythonEnvironmentStepUtils';
import { DropdownEntry } from 'vs/workbench/browser/positronNewProjectWizard/components/steps/dropdownEntry';

/**
* PythonInterpreterEntryProps interface.
Expand All @@ -26,14 +27,12 @@ interface PythonInterpreterEntryProps {
export const PythonInterpreterEntry = ({ pythonInterpreterInfo }: PythonInterpreterEntryProps) => {
// Render.
return (
<div className='python-interpreter-entry'>
<div className='interpreter-title'>
{/* allow-any-unicode-next-line */}
{`${pythonInterpreterInfo.preferred ? '★ ' : ''}${pythonInterpreterInfo.languageName} ${pythonInterpreterInfo.languageVersion} ${pythonInterpreterInfo.runtimePath}`}
</div>
<div className='interpreter-source'>
{pythonInterpreterInfo.runtimeSource}
</div>
</div>
<DropdownEntry
// allow-any-unicode-next-line
icon={pythonInterpreterInfo.preferred ? '★' : ''}
title={`${pythonInterpreterInfo.languageName} ${pythonInterpreterInfo.languageVersion}`}
subtitle={`${pythonInterpreterInfo.runtimePath}`}
group={pythonInterpreterInfo.runtimeSource}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,27 @@ export enum EnvironmentSetupType {
NewEnvironment = 'newEnvironment',
ExistingEnvironment = 'existingEnvironment'
}

/**
* PythonEnvironmentType enum includes the types of Python environments.
* - Venv: A virtual environment.
* - Conda: A conda environment.
* TODO: retrieve these values from the appropriate extensions/services?
*/
export enum PythonEnvironmentType {
Venv = 'Venv',
Conda = 'Conda'
}

/**
* NewProjectType enum. Defines the types of projects that can be created.
* TODO: localize. Since this is an enum, we can't use the localize function
* because computed values must be numbers (not strings). So we'll probably need to
* turn this into an object with keys and values, maybe also using something like
* satisfies Readonly<Record<string, string>>.
*/
export enum NewProjectType {
PythonProject = 'Python Project',
RProject = 'R Project',
JupyterNotebook = 'Jupyter Notebook'
}
Loading
Loading