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
1 change: 1 addition & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ export namespace CreateEnv {
);
export const deletingEnvironmentProgress = l10n.t('Deleting existing ".venv" environment...');
export const errorDeletingEnvironment = l10n.t('Error while deleting existing ".venv" environment.');
export const openRequirementsFile = l10n.t('Open requirements file');
}

export namespace Conda {
Expand Down
12 changes: 12 additions & 0 deletions src/client/common/vscodeApis/windowApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ import {
TextEditor,
window,
Disposable,
QuickPickItemButtonEvent,
Uri,
} from 'vscode';
import { createDeferred, Deferred } from '../utils/async';

export function showTextDocument(uri: Uri): Thenable<TextEditor> {
return window.showTextDocument(uri);
}

export function showQuickPick<T extends QuickPickItem>(
items: readonly T[] | Thenable<readonly T[]>,
options?: QuickPickOptions,
Expand Down Expand Up @@ -91,6 +97,7 @@ export async function showQuickPickWithBack<T extends QuickPickItem>(
items: readonly T[],
options?: QuickPickOptions,
token?: CancellationToken,
itemButtonHandler?: (e: QuickPickItemButtonEvent<T>) => void,
): Promise<T | T[] | undefined> {
const quickPick: QuickPick<T> = window.createQuickPick<T>();
const disposables: Disposable[] = [quickPick];
Expand Down Expand Up @@ -130,6 +137,11 @@ export async function showQuickPickWithBack<T extends QuickPickItem>(
deferred.resolve(undefined);
}
}),
quickPick.onDidTriggerItemButton((e) => {
if (itemButtonHandler) {
itemButtonHandler(e);
}
}),
);
if (token) {
disposables.push(
Expand Down
40 changes: 32 additions & 8 deletions src/client/pythonEnvironments/creation/provider/venvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import * as tomljs from '@iarna/toml';
import * as fs from 'fs-extra';
import { flatten, isArray } from 'lodash';
import * as path from 'path';
import { CancellationToken, ProgressLocation, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode';
import {
CancellationToken,
ProgressLocation,
QuickPickItem,
QuickPickItemButtonEvent,
RelativePattern,
ThemeIcon,
Uri,
WorkspaceFolder,
} from 'vscode';
import { Common, CreateEnv } from '../../../common/utils/localize';
import {
MultiStepAction,
MultiStepNode,
showQuickPickWithBack,
showTextDocument,
withProgress,
} from '../../../common/vscodeApis/windowApis';
import { findFiles } from '../../../common/vscodeApis/workspaceApis';
Expand All @@ -20,6 +30,10 @@ import { isWindows } from '../../../common/platform/platformService';
import { getVenvPath, hasVenv } from '../common/commonUtils';
import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils';

export const OPEN_REQUIREMENTS_BUTTON = {
iconPath: new ThemeIcon('go-to-file'),
tooltip: CreateEnv.Venv.openRequirementsFile,
};
const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**';
async function getPipRequirementsFiles(
workspaceFolder: WorkspaceFolder,
Expand Down Expand Up @@ -78,8 +92,13 @@ async function pickTomlExtras(extras: string[], token?: CancellationToken): Prom
return undefined;
}

async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise<string[] | undefined> {
async function pickRequirementsFiles(
files: string[],
root: string,
token?: CancellationToken,
): Promise<string[] | undefined> {
const items: QuickPickItem[] = files
.map((p) => path.relative(root, p))
.sort((a, b) => {
const al: number = a.split(/[\\\/]/).length;
const bl: number = b.split(/[\\\/]/).length;
Expand All @@ -91,7 +110,10 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken)
}
return al - bl;
})
.map((e) => ({ label: e }));
.map((e) => ({
label: e,
buttons: [OPEN_REQUIREMENTS_BUTTON],
}));

const selection = await showQuickPickWithBack(
items,
Expand All @@ -101,6 +123,11 @@ async function pickRequirementsFiles(files: string[], token?: CancellationToken)
canPickMany: true,
},
token,
async (e: QuickPickItemButtonEvent<QuickPickItem>) => {
if (e.item.label) {
await showTextDocument(Uri.file(path.join(root, e.item.label)));
}
},
);

if (selection && isArray(selection)) {
Expand Down Expand Up @@ -195,14 +222,11 @@ export async function pickPackagesToInstall(
tomlStep,
async (context?: MultiStepAction) => {
traceVerbose('Looking for pip requirements.');
const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) =>
path.relative(workspaceFolder.uri.fsPath, p),
);

const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token);
if (requirementFiles && requirementFiles.length > 0) {
traceVerbose('Found pip requirements.');
try {
const result = await pickRequirementsFiles(requirementFiles, token);
const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token);
const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p));
if (installList) {
installList.forEach((i) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import * as windowApis from '../../../../client/common/vscodeApis/windowApis';
import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis';
import {
ExistingVenvAction,
OPEN_REQUIREMENTS_BUTTON,
pickExistingVenvAction,
pickPackagesToInstall,
} from '../../../../client/pythonEnvironments/creation/provider/venvUtils';
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants';
import { CreateEnv } from '../../../../client/common/utils/localize';
import { createDeferred } from '../../../../client/common/utils/async';

chaiUse(chaiAsPromised);

Expand All @@ -23,6 +25,7 @@ suite('Venv Utils test', () => {
let showQuickPickWithBackStub: sinon.SinonStub;
let pathExistsStub: sinon.SinonStub;
let readFileStub: sinon.SinonStub;
let showTextDocumentStub: sinon.SinonStub;

const workspace1 = {
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')),
Expand All @@ -35,6 +38,7 @@ suite('Venv Utils test', () => {
showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack');
pathExistsStub = sinon.stub(fs, 'pathExists');
readFileStub = sinon.stub(fs, 'readFile');
showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument');
});

teardown(() => {
Expand Down Expand Up @@ -224,13 +228,18 @@ suite('Venv Utils test', () => {
await assert.isRejected(pickPackagesToInstall(workspace1));
assert.isTrue(
showQuickPickWithBackStub.calledWithExactly(
[{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }],
[
{ label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
],
{
placeHolder: CreateEnv.Venv.requirementsQuickPickTitle,
ignoreFocusOut: true,
canPickMany: true,
},
undefined,
sinon.match.func,
),
);
assert.isTrue(readFileStub.calledOnce);
Expand All @@ -257,13 +266,18 @@ suite('Venv Utils test', () => {
const actual = await pickPackagesToInstall(workspace1);
assert.isTrue(
showQuickPickWithBackStub.calledWithExactly(
[{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }],
[
{ label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
],
{
placeHolder: CreateEnv.Venv.requirementsQuickPickTitle,
ignoreFocusOut: true,
canPickMany: true,
},
undefined,
sinon.match.func,
),
);
assert.deepStrictEqual(actual, []);
Expand All @@ -290,13 +304,18 @@ suite('Venv Utils test', () => {
const actual = await pickPackagesToInstall(workspace1);
assert.isTrue(
showQuickPickWithBackStub.calledWithExactly(
[{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }],
[
{ label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
],
{
placeHolder: CreateEnv.Venv.requirementsQuickPickTitle,
ignoreFocusOut: true,
canPickMany: true,
},
undefined,
sinon.match.func,
),
);
assert.deepStrictEqual(actual, [
Expand Down Expand Up @@ -328,13 +347,18 @@ suite('Venv Utils test', () => {
const actual = await pickPackagesToInstall(workspace1);
assert.isTrue(
showQuickPickWithBackStub.calledWithExactly(
[{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }],
[
{ label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
{ label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] },
],
{
placeHolder: CreateEnv.Venv.requirementsQuickPickTitle,
ignoreFocusOut: true,
canPickMany: true,
},
undefined,
sinon.match.func,
),
);
assert.deepStrictEqual(actual, [
Expand All @@ -349,6 +373,45 @@ suite('Venv Utils test', () => {
]);
assert.isTrue(readFileStub.notCalled);
});

test('User clicks button to open requirements.txt', async () => {
let allow = true;
findFilesStub.callsFake(() => {
if (allow) {
allow = false;
return Promise.resolve([
Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')),
Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')),
Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')),
]);
}
return Promise.resolve([]);
});
pathExistsStub.resolves(false);

const deferred = createDeferred();
showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => {
callback({
button: OPEN_REQUIREMENTS_BUTTON,
item: { label: 'requirements.txt' },
});
await deferred.promise;
return [{ label: 'requirements.txt' }];
});

let uri: Uri | undefined;
showTextDocumentStub.callsFake((arg: Uri) => {
uri = arg;
deferred.resolve();
return Promise.resolve();
});

await pickPackagesToInstall(workspace1);
assert.deepStrictEqual(
uri?.toString(),
Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(),
);
});
});

suite('Test pick existing venv action', () => {
Expand Down