Skip to content

Commit

Permalink
Open requirement files (microsoft#21917)
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiknadig authored Sep 13, 2023
1 parent 9ebc5eb commit 221b769
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 12 deletions.
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

0 comments on commit 221b769

Please sign in to comment.