Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/common/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export namespace Pickers {
export namespace Environments {
export const selectExecutable = l10n.t('Select Python Executable');
export const selectEnvironment = l10n.t('Select a Python Environment');
export const refresh = l10n.t('Refresh environments');
export const refreshFailed = l10n.t(
'Failed to refresh Python environments. Check the Python Environments logs for more details.',
);
}

export namespace Packages {
Expand Down
266 changes: 195 additions & 71 deletions src/common/pickers/environments.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { ProgressLocation, QuickInputButtons, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri, l10n } from 'vscode';
import {
ProgressLocation,
QuickInputButton,
QuickInputButtons,
QuickPick,
QuickPickItem,
QuickPickItemKind,
ThemeIcon,
Uri,
l10n,
} from 'vscode';
import { CreateEnvironmentOptions, IconPath, PythonEnvironment, PythonProject } from '../../api';
import type { InternalEnvironmentManager } from '../../managers/common/registeredManagers';
import { showErrorMessageWithLogs } from '../errors/utils';
import { Common, Interpreter, Pickers } from '../localize';
import { traceError } from '../logging';
import { EventNames } from '../telemetry/constants';
Expand All @@ -27,6 +38,17 @@ type QuickPickIcon =
| ThemeIcon
| undefined;

interface EnvironmentPickItem extends QuickPickItem {
readonly result?: PythonEnvironment;
}

const REFRESH_ENVIRONMENTS_BUTTON: QuickInputButton = {
iconPath: new ThemeIcon('refresh'),
tooltip: Pickers.Environments.refresh,
};
const inFlightManagerRefreshes = new WeakMap<InternalEnvironmentManager, Promise<void>>();
let refreshErrorNotificationShown = false;

function getIconPath(i: IconPath | undefined): QuickPickIcon {
if (i === undefined || i instanceof ThemeIcon || i instanceof Uri) {
return i;
Expand All @@ -47,6 +69,158 @@ interface EnvironmentPickOptions {
showBackButton?: boolean;
projects: PythonProject[];
}

function isSameEnvironment(left: PythonEnvironment, right: PythonEnvironment): boolean {
return left.envId.id === right.envId.id && left.envId.managerId === right.envId.managerId;
}

function getEnvironmentDescription(environment: PythonEnvironment): string {
return environment.description?.trim()
? `${environment.description} (${environment.displayPath})`
: environment.displayPath;
}

function createEnvironmentPickItem(
environment: PythonEnvironment,
): EnvironmentPickItem {
return {
label: environment.displayName ?? environment.name,
description: getEnvironmentDescription(environment),
result: environment,
iconPath: getIconPath(environment.iconPath),
};
}

async function buildEnvironmentPickItems(
managers: InternalEnvironmentManager[],
options: EnvironmentPickOptions,
): Promise<EnvironmentPickItem[]> {
const items: EnvironmentPickItem[] = [
{
label: Interpreter.enterInterpreterPath,
iconPath: new ThemeIcon('edit'),
},
{
label: Interpreter.browsePath,
iconPath: new ThemeIcon('folder'),
},
];

if (getWorkspaceFolders()?.length) {
items.push(
{
label: '',
kind: QuickPickItemKind.Separator,
},
{
label: Interpreter.createVirtualEnvironment,
iconPath: new ThemeIcon('add'),
},
);
}

if (options.recommended) {
items.push(
{
label: Common.recommended,
kind: QuickPickItemKind.Separator,
},
createEnvironmentPickItem(options.recommended),
);
}

for (const manager of managers) {
items.push({
label: manager.displayName,
kind: QuickPickItemKind.Separator,
});
const environments = await manager.getEnvironments('all');
items.push(...environments.map((environment) => createEnvironmentPickItem(environment)));
}

return items;
}

function findEquivalentPickItem(
items: readonly EnvironmentPickItem[],
previous: EnvironmentPickItem | undefined,
): EnvironmentPickItem | undefined {
if (previous?.result) {
const previousResult = previous.result;
return items.find((item) => item.result !== undefined && isSameEnvironment(item.result, previousResult));
}
return previous ? items.find((item) => item.label === previous.label) : undefined;
}

function refreshManager(manager: InternalEnvironmentManager): Promise<void> {
const inFlight = inFlightManagerRefreshes.get(manager);
if (inFlight) {
return inFlight;
}

const refresh = manager.refresh(undefined);
inFlightManagerRefreshes.set(manager, refresh);
const clearRefresh = () => {
if (inFlightManagerRefreshes.get(manager) === refresh) {
inFlightManagerRefreshes.delete(manager);
}
};
void refresh.then(clearRefresh, clearRefresh);
return refresh;
}

async function refreshEnvironmentPickItems(
managers: InternalEnvironmentManager[],
options: EnvironmentPickOptions,
quickPick: QuickPick<EnvironmentPickItem>,
): Promise<void> {
let hidden = false;
const hideDisposable = quickPick.onDidHide(() => {
hidden = true;
});
const activeItem = quickPick.activeItems[0];
quickPick.busy = true;
quickPick.enabled = false;

try {
await Promise.all(managers.map((manager) => refreshManager(manager)));
if (hidden) {
return;
}

const items = await buildEnvironmentPickItems(managers, options);
if (hidden) {
return;
}

quickPick.keepScrollPosition = true;
quickPick.items = items;
const recommended = options.recommended;
const preservedItem =
findEquivalentPickItem(items, activeItem) ??
(recommended
? items.find((item) => item.result !== undefined && isSameEnvironment(item.result, recommended))
: undefined);
if (preservedItem) {
quickPick.activeItems = [preservedItem];
}
} catch (error) {
traceError('Failed to refresh environments from the environment picker:', error);
if (!hidden && !refreshErrorNotificationShown) {
refreshErrorNotificationShown = true;
void showErrorMessageWithLogs(Pickers.Environments.refreshFailed).catch((notificationError) =>
traceError('Failed to show the environment refresh error notification:', notificationError),
);
}
} finally {
hideDisposable.dispose();
if (!hidden) {
quickPick.busy = false;
quickPick.enabled = true;
}
}
}

async function browseForPython(
managers: InternalEnvironmentManager[],
projectEnvManagers: InternalEnvironmentManager[],
Expand Down Expand Up @@ -144,15 +318,33 @@ async function createEnvironment(
}

async function pickEnvironmentImpl(
items: (QuickPickItem | (QuickPickItem & { result: PythonEnvironment }))[],
managers: InternalEnvironmentManager[],
projectEnvManagers: InternalEnvironmentManager[],
options: EnvironmentPickOptions,
): Promise<PythonEnvironment | undefined> {
const items = await buildEnvironmentPickItems(managers, options);
let refreshInProgress: Promise<void> | undefined;
const selected = await showQuickPickWithButtons(items, {
placeHolder: Pickers.Environments.selectEnvironment,
ignoreFocusOut: true,
showBackButton: options?.showBackButton,
buttons: [REFRESH_ENVIRONMENTS_BUTTON],
buttonHandler: (button, quickPick) => {
if (button !== REFRESH_ENVIRONMENTS_BUTTON) {
return;
}
if (!refreshInProgress) {
const refresh = refreshEnvironmentPickItems(managers, options, quickPick);
refreshInProgress = refresh;
const clearRefresh = () => {
if (refreshInProgress === refresh) {
refreshInProgress = undefined;
}
};
void refresh.then(clearRefresh, clearRefresh);
}
return refreshInProgress;
},
});

if (selected && !Array.isArray(selected)) {
Expand All @@ -177,75 +369,7 @@ export async function pickEnvironment(
projectEnvManagers: InternalEnvironmentManager[],
options: EnvironmentPickOptions,
): Promise<PythonEnvironment | undefined> {
const items: (QuickPickItem | (QuickPickItem & { result: PythonEnvironment }))[] = [
{
label: Interpreter.enterInterpreterPath,
iconPath: new ThemeIcon('edit'),
},
{
label: Interpreter.browsePath,
iconPath: new ThemeIcon('folder'),
},
];

if (getWorkspaceFolders()?.length) {
items.push(
{
label: '',
kind: QuickPickItemKind.Separator,
},
{
label: Interpreter.createVirtualEnvironment,
iconPath: new ThemeIcon('add'),
},
);
}

if (options?.recommended) {
const pathDescription = options.recommended.displayPath;
const description =
options.recommended.description && options.recommended.description.trim()
? `${options.recommended.description} (${pathDescription})`
: pathDescription;

items.push(
{
label: Common.recommended,
kind: QuickPickItemKind.Separator,
},
{
label: options.recommended.displayName,
description: description,
result: options.recommended,
iconPath: getIconPath(options.recommended.iconPath),
},
);
}

for (const manager of managers) {
items.push({
label: manager.displayName,
kind: QuickPickItemKind.Separator,
});
const envs = await manager.getEnvironments('all');
items.push(
...envs.map((e) => {
const pathDescription = e.displayPath;
const description =
e.description && e.description.trim() ? `${e.description} (${pathDescription})` : pathDescription;

return {
label: e.displayName ?? e.name,
description: description,
result: e,
manager: manager,
iconPath: getIconPath(e.iconPath),
};
}),
);
}

return pickEnvironmentImpl(items, managers, projectEnvManagers, options);
return pickEnvironmentImpl(managers, projectEnvManagers, options);
}

export async function pickEnvironmentFrom(environments: PythonEnvironment[]): Promise<PythonEnvironment | undefined> {
Expand Down
25 changes: 22 additions & 3 deletions src/common/window.apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ export interface QuickPickButtonEvent<T extends QuickPickItem> {
readonly button: QuickInputButton;
}

export interface QuickPickWithButtonsOptions<T extends QuickPickItem> extends QuickPickOptions {
showBackButton?: boolean;
buttons?: QuickInputButton[];
selected?: T[];
buttonHandler?: (button: QuickInputButton, quickPick: QuickPick<T>) => void | Thenable<void>;
}

export function showQuickPick<T extends QuickPickItem>(
items: readonly T[] | Thenable<readonly T[]>,
options?: QuickPickOptions,
Expand All @@ -175,7 +182,7 @@ export function withProgress<R>(

export async function showQuickPickWithButtons<T extends QuickPickItem>(
items: readonly T[],
options?: QuickPickOptions & { showBackButton?: boolean; buttons?: QuickInputButton[]; selected?: T[] },
options?: QuickPickWithButtonsOptions<T>,
token?: CancellationToken,
itemButtonHandler?: (e: QuickPickItemButtonEvent<T>) => void,
): Promise<T | T[] | undefined> {
Expand Down Expand Up @@ -206,8 +213,20 @@ export async function showQuickPickWithButtons<T extends QuickPickItem>(
deferred.reject(QuickInputButtons.Back);
quickPick.hide();
} else if (options?.buttons?.includes(button)) {
deferred.reject({ item: quickPick.selectedItems, button });
quickPick.hide();
if (options.buttonHandler) {
const buttonHandler = options.buttonHandler;
void Promise.resolve()
.then(() => buttonHandler(button, quickPick))
.catch((error) => {
if (!deferred.completed) {
deferred.reject(error);
quickPick.hide();
}
});
} else {
deferred.reject({ item: quickPick.selectedItems, button });
quickPick.hide();
}
}
}),
quickPick.onDidAccept(() => {
Expand Down
Loading
Loading