Skip to content

Expose interpreter quickpick API with filtering #19839

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

Merged
merged 5 commits into from
Sep 15, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import {
IQuickPickParameters,
} from '../../../../common/utils/multiStepInput';
import { SystemVariables } from '../../../../common/variables/systemVariables';
import { EnvironmentType } from '../../../../pythonEnvironments/info';
import { EnvironmentType, PythonEnvironment } from '../../../../pythonEnvironments/info';
import { captureTelemetry, sendTelemetryEvent } from '../../../../telemetry';
import { EventName } from '../../../../telemetry/constants';
import { IInterpreterService, PythonEnvironmentsChangedEvent } from '../../../contracts';
import { isProblematicCondaEnvironment } from '../../environmentTypeComparer';
import {
IInterpreterQuickPick,
IInterpreterQuickPickItem,
IInterpreterSelector,
IPythonPathUpdaterServiceManager,
Expand Down Expand Up @@ -69,7 +70,7 @@ export namespace EnvGroups {
}

@injectable()
export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implements IInterpreterQuickPick {
private readonly manualEntrySuggestion: ISpecialQuickPickItem = {
label: `${Octicons.Add} ${InterpreterQuickPickList.enterPath.label}`,
alwaysShow: true,
Expand Down Expand Up @@ -126,11 +127,12 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
public async _pickInterpreter(
input: IMultiStepInput<InterpreterStateArgs>,
state: InterpreterStateArgs,
filter?: (i: PythonEnvironment) => boolean,
): Promise<void | InputStep<InterpreterStateArgs>> {
// If the list is refreshing, it's crucial to maintain sorting order at all
// times so that the visible items do not change.
const preserveOrderWhenFiltering = !!this.interpreterService.refreshPromise;
const suggestions = this._getItems(state.workspace);
const suggestions = this._getItems(state.workspace, filter);
state.path = undefined;
const currentInterpreterPathDisplay = this.pathUtils.getDisplayName(
this.configurationService.getSettings(state.workspace).pythonPath,
Expand Down Expand Up @@ -179,10 +181,10 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
// Items are in the final state as all previous callbacks have finished executing.
quickPick.busy = false;
// Ensure we set a recommended item after refresh has finished.
this.updateQuickPickItems(quickPick, {}, state.workspace);
this.updateQuickPickItems(quickPick, {}, state.workspace, filter);
});
}
this.updateQuickPickItems(quickPick, event, state.workspace);
this.updateQuickPickItems(quickPick, event, state.workspace, filter);
},
},
});
Expand All @@ -204,26 +206,33 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
return undefined;
}

public _getItems(resource: Resource): QuickPickType[] {
public _getItems(resource: Resource, filter: ((i: PythonEnvironment) => boolean) | undefined): QuickPickType[] {
const suggestions: QuickPickType[] = [this.manualEntrySuggestion];
const defaultInterpreterPathSuggestion = this.getDefaultInterpreterPathSuggestion(resource);
if (defaultInterpreterPathSuggestion) {
suggestions.push(defaultInterpreterPathSuggestion);
}
const interpreterSuggestions = this.getSuggestions(resource);
const interpreterSuggestions = this.getSuggestions(resource, filter);
this.finalizeItems(interpreterSuggestions, resource);
suggestions.push(...interpreterSuggestions);
return suggestions;
}

private getSuggestions(resource: Resource): QuickPickType[] {
private getSuggestions(
resource: Resource,
filter: ((i: PythonEnvironment) => boolean) | undefined,
): QuickPickType[] {
const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource);
const items = this.interpreterSelector.getSuggestions(resource, !!this.interpreterService.refreshPromise);
const items = this.interpreterSelector
.getSuggestions(resource, !!this.interpreterService.refreshPromise)
.filter((i) => !filter || filter(i.interpreter));
if (this.interpreterService.refreshPromise) {
// We cannot put items in groups while the list is loading as group of an item can change.
return items;
}
const itemsWithFullName = this.interpreterSelector.getSuggestions(resource, true);
const itemsWithFullName = this.interpreterSelector
.getSuggestions(resource, true)
.filter((i) => !filter || filter(i.interpreter));
const recommended = this.interpreterSelector.getRecommendedSuggestion(
itemsWithFullName,
this.workspaceService.getWorkspaceFolder(resource)?.uri,
Expand Down Expand Up @@ -277,10 +286,11 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
quickPick: QuickPick<QuickPickType>,
event: PythonEnvironmentsChangedEvent,
resource: Resource,
filter: ((i: PythonEnvironment) => boolean) | undefined,
) {
// Active items are reset once we replace the current list with updated items, so save it.
const activeItemBeforeUpdate = quickPick.activeItems.length > 0 ? quickPick.activeItems[0] : undefined;
quickPick.items = this.getUpdatedItems(quickPick.items, event, resource);
quickPick.items = this.getUpdatedItems(quickPick.items, event, resource, filter);
// Ensure we maintain the same active item as before.
const activeItem = activeItemBeforeUpdate
? quickPick.items.find((item) => {
Expand All @@ -304,10 +314,14 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
items: readonly QuickPickType[],
event: PythonEnvironmentsChangedEvent,
resource: Resource,
filter: ((i: PythonEnvironment) => boolean) | undefined,
): QuickPickType[] {
const updatedItems = [...items.values()];
const areItemsGrouped = items.find((item) => isSeparatorItem(item));
const env = event.old ?? event.new;
if (filter && event.new && !filter(event.new)) {
event.new = undefined; // Remove envs we're not looking for from the list.
}
let envIndex = -1;
if (env) {
envIndex = updatedItems.findIndex(
Expand Down Expand Up @@ -476,7 +490,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
const wkspace = targetConfig[0].folderUri;
const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace };
const multiStep = this.multiStepFactory.create<InterpreterStateArgs>();
await multiStep.run((input, s) => this._pickInterpreter(input, s), interpreterState);
await multiStep.run((input, s) => this._pickInterpreter(input, s, undefined), interpreterState);

if (interpreterState.path !== undefined) {
// User may choose to have an empty string stored, so variable `interpreterState.path` may be
Expand All @@ -486,6 +500,16 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
}
}

public async getInterpreterViaQuickPick(
workspace: Resource,
filter: ((i: PythonEnvironment) => boolean) | undefined,
): Promise<string | undefined> {
const interpreterState: InterpreterStateArgs = { path: undefined, workspace };
const multiStep = this.multiStepFactory.create<InterpreterStateArgs>();
await multiStep.run((input, s) => this._pickInterpreter(input, s, filter), interpreterState);
return interpreterState.path;
}

/**
* Check if the interpreter that was entered exists in the list of suggestions.
* If it does, it means that it had already been discovered,
Expand All @@ -495,7 +519,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand {
*/
// eslint-disable-next-line class-methods-use-this
private sendInterpreterEntryTelemetry(selection: string, workspace: Resource): void {
const suggestions = this._getItems(workspace);
const suggestions = this._getItems(workspace, undefined);
let interpreterPath = path.normalize(untildify(selection));

if (!path.isAbsolute(interpreterPath)) {
Expand Down
8 changes: 8 additions & 0 deletions src/client/interpreter/configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ export interface IInterpreterComparer {
compare(a: PythonEnvironment, b: PythonEnvironment): number;
getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined;
}

export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick');
export interface IInterpreterQuickPick {
getInterpreterViaQuickPick(
workspace: Resource,
filter?: (i: PythonEnvironment) => boolean,
): Promise<string | undefined>;
}
2 changes: 2 additions & 0 deletions src/client/interpreter/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterServi
import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory';
import {
IInterpreterComparer,
IInterpreterQuickPick,
IInterpreterSelector,
IPythonPathUpdaterServiceFactory,
IPythonPathUpdaterServiceManager,
Expand Down Expand Up @@ -62,6 +63,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void
IExtensionSingleActivationService,
SetShebangInterpreterCommand,
);
serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand);

serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, VirtualEnvironmentPrompt);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,121 @@ suite('Set Interpreter Command', () => {
assert.deepStrictEqual(actualParameters?.items, expectedParameters.items, 'Params not equal');
});

test('Items displayed should be filtered out if a filter is provided', async () => {
const state: InterpreterStateArgs = { path: 'some path', workspace: undefined };
const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>();
const interpreterItems: IInterpreterQuickPickItem[] = [
{
description: `${workspacePath}/interpreterPath1`,
detail: '',
label: 'This is the selected Python path',
path: `${workspacePath}/interpreterPath1`,
interpreter: {
id: `${workspacePath}/interpreterPath1`,
path: `${workspacePath}/interpreterPath1`,
envType: EnvironmentType.Venv,
} as PythonEnvironment,
},
{
description: 'interpreterPath2',
detail: '',
label: 'This is the selected Python path',
path: 'interpreterPath2',
interpreter: {
id: 'interpreterPath2',
path: 'interpreterPath2',
envType: EnvironmentType.VirtualEnvWrapper,
} as PythonEnvironment,
},
{
description: 'interpreterPath3',
detail: '',
label: 'This is the selected Python path',
path: 'interpreterPath3',
interpreter: {
id: 'interpreterPath3',
path: 'interpreterPath3',
envType: EnvironmentType.VirtualEnvWrapper,
} as PythonEnvironment,
},
{
description: 'interpreterPath4',
detail: '',
label: 'This is the selected Python path',
path: 'interpreterPath4',
interpreter: {
path: 'interpreterPath4',
id: 'interpreterPath4',
envType: EnvironmentType.Conda,
} as PythonEnvironment,
},
item,
{
description: 'interpreterPath5',
detail: '',
label: 'This is the selected Python path',
path: 'interpreterPath5',
interpreter: {
path: 'interpreterPath5',
id: 'interpreterPath5',
envType: EnvironmentType.Global,
} as PythonEnvironment,
},
];
interpreterSelector.reset();
interpreterSelector
.setup((i) => i.getSuggestions(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => interpreterItems);
interpreterSelector
.setup((i) => i.getRecommendedSuggestion(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => item);
const recommended = cloneDeep(item);
recommended.label = `${Octicons.Star} ${item.label}`;
recommended.description = interpreterPath;
const suggestions = [
expectedEnterInterpreterPathSuggestion,
defaultInterpreterPathSuggestion,
{ kind: QuickPickItemKind.Separator, label: EnvGroups.Recommended },
recommended,
{ label: EnvGroups.VirtualEnvWrapper, kind: QuickPickItemKind.Separator },
interpreterItems[1],
interpreterItems[2],
{ label: EnvGroups.Global, kind: QuickPickItemKind.Separator },
interpreterItems[5],
];
const expectedParameters: IQuickPickParameters<QuickPickItem> = {
placeholder: `Selected Interpreter: ${currentPythonPath}`,
items: suggestions,
activeItem: recommended,
matchOnDetail: true,
matchOnDescription: true,
title: InterpreterQuickPickList.browsePath.openButtonLabel,
sortByLabel: true,
keepScrollPosition: true,
};
let actualParameters: IQuickPickParameters<QuickPickItem> | 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,
(e) => e.envType === EnvironmentType.VirtualEnvWrapper || e.envType === EnvironmentType.Global,
);

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;
assert.deepStrictEqual(actualParameters?.items, expectedParameters.items, 'Params not equal');
});

test('If system variables are used in the default interpreter path, make sure they are resolved when the path is displayed', async () => {
// Create a SetInterpreterCommand instance from scratch, and use a different defaultInterpreterPath from the rest of the tests.
const workspaceDefaultInterpreterPath = '${workspaceFolder}/defaultInterpreterPath';
Expand Down
3 changes: 2 additions & 1 deletion src/test/interpreters/serviceRegistry.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { PythonPathUpdaterService } from '../../client/interpreter/configuration
import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory';
import {
IInterpreterComparer,
IInterpreterQuickPick,
IInterpreterSelector,
IPythonPathUpdaterServiceFactory,
IPythonPathUpdaterServiceManager,
Expand Down Expand Up @@ -53,6 +54,7 @@ suite('Interpreters - Service Registry', () => {
[IExtensionSingleActivationService, InstallPythonCommand],
[IExtensionSingleActivationService, InstallPythonViaTerminal],
[IExtensionSingleActivationService, SetInterpreterCommand],
[IInterpreterQuickPick, SetInterpreterCommand],
[IExtensionSingleActivationService, ResetInterpreterCommand],
[IExtensionSingleActivationService, SetShebangInterpreterCommand],

Expand All @@ -63,7 +65,6 @@ suite('Interpreters - Service Registry', () => {

[IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory],
[IPythonPathUpdaterServiceManager, PythonPathUpdaterService],

[IInterpreterSelector, InterpreterSelector],
[IShebangCodeLensProvider, ShebangCodeLensProvider],
[IInterpreterHelper, InterpreterHelper],
Expand Down