Skip to content

Commit

Permalink
Expose interpreter quickpick API with filtering (#19839)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kartik Raj authored Sep 15, 2022
1 parent 8e91332 commit cb3c629
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 14 deletions.
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

0 comments on commit cb3c629

Please sign in to comment.