Skip to content

Commit 68648f4

Browse files
interpreter: hide environment creation without a workspace folder (#1845)
- Hide the Create Environment action (and its separator) in the interpreter picker when no workspace folder is open, so users cannot enter an inapplicable creation flow. Fixes #395 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e805665 commit 68648f4

3 files changed

Lines changed: 83 additions & 8 deletions

File tree

‎docs/managing-python-projects.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ By default, every workspace folder you open in VS Code is automatically treated
1212
- Add individual Python files as projects (great for standalone scripts)
1313
- Create brand new projects from templates
1414

15+
The **Create Environment** action in the interpreter picker is available only when at least one workspace folder is open. Without an open folder, you can still select an existing interpreter or browse for one. Explicit global-environment creation through the **Create Environment** command or the **Environment Managers** view remains available.
16+
1517
### Why use projects?
1618

1719
Projects solve a common challenge: **different parts of your workspace need different Python environments**.

‎src/common/pickers/environments.ts‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
showQuickPickWithButtons,
1515
withProgress,
1616
} from '../window.apis';
17+
import { getWorkspaceFolders } from '../workspace.apis';
1718
import { pickEnvironmentManager } from './managers';
1819

1920
type QuickPickIcon =
@@ -157,16 +158,21 @@ export async function pickEnvironment(
157158
label: Interpreter.browsePath,
158159
iconPath: new ThemeIcon('folder'),
159160
},
160-
{
161-
label: '',
162-
kind: QuickPickItemKind.Separator,
163-
},
164-
{
165-
label: Interpreter.createVirtualEnvironment,
166-
iconPath: new ThemeIcon('add'),
167-
},
168161
];
169162

163+
if (getWorkspaceFolders()?.length) {
164+
items.push(
165+
{
166+
label: '',
167+
kind: QuickPickItemKind.Separator,
168+
},
169+
{
170+
label: Interpreter.createVirtualEnvironment,
171+
iconPath: new ThemeIcon('add'),
172+
},
173+
);
174+
}
175+
170176
if (options?.recommended) {
171177
const pathDescription = options.recommended.displayPath;
172178
const description =

‎src/test/common/environmentPicker.unit.test.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,75 @@
22
// Licensed under the MIT License.
33

44
import assert from 'node:assert';
5+
import * as path from 'path';
6+
import * as sinon from 'sinon';
57
import { Uri } from 'vscode';
68
import { PythonEnvironment } from '../../api';
9+
import { Interpreter } from '../../common/localize';
10+
import { pickEnvironment } from '../../common/pickers/environments';
11+
import * as managerPicker from '../../common/pickers/managers';
12+
import * as windowApis from '../../common/window.apis';
13+
import * as workspaceApis from '../../common/workspace.apis';
14+
import { createMockPythonEnvironment } from '../mocks/pythonEnvironment';
15+
16+
suite('Environment Picker Creation Availability', () => {
17+
const folder = { uri: Uri.file(process.cwd()), name: 'workspace', index: 0 };
18+
19+
teardown(() => {
20+
sinon.restore();
21+
});
22+
23+
for (const { name, folders, canCreate } of [
24+
{ name: 'no workspace', folders: undefined, canCreate: false },
25+
{ name: 'an empty workspace', folders: [], canCreate: false },
26+
{ name: 'an open folder without detected projects', folders: [folder], canCreate: true },
27+
{
28+
name: 'a multi-root workspace',
29+
folders: [folder, { uri: Uri.file(path.join(process.cwd(), 'second')), name: 'second', index: 1 }],
30+
canCreate: true,
31+
},
32+
]) {
33+
test(`only offers creation with an open folder: ${name}`, async () => {
34+
sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(folders);
35+
const pickManager = sinon.stub(managerPicker, 'pickEnvironmentManager');
36+
const picker = sinon.stub(windowApis, 'showQuickPickWithButtons').callsFake(async (items) => {
37+
assert.strictEqual(
38+
items.some((item) => item.label === Interpreter.createVirtualEnvironment),
39+
canCreate,
40+
);
41+
assert.ok(items.some((item) => item.label === Interpreter.browsePath));
42+
return undefined;
43+
});
44+
45+
assert.strictEqual(await pickEnvironment([], [], { projects: [] }), undefined);
46+
sinon.assert.calledOnce(picker);
47+
sinon.assert.notCalled(pickManager);
48+
});
49+
}
50+
51+
test('still selects an existing environment without an open folder', async () => {
52+
const environment = createMockPythonEnvironment({ envPath: path.join(process.cwd(), 'python') });
53+
sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined);
54+
sinon
55+
.stub(windowApis, 'showQuickPickWithButtons')
56+
.callsFake(async (items) => items.find((item) => item.label === environment.displayName));
57+
58+
const selected = await pickEnvironment([], [], { projects: [], recommended: environment });
59+
60+
assert.strictEqual(selected, environment);
61+
});
62+
63+
test('still allows browsing for an interpreter without an open folder', async () => {
64+
sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined);
65+
sinon
66+
.stub(windowApis, 'showQuickPickWithButtons')
67+
.callsFake(async (items) => items.find((item) => item.label === Interpreter.browsePath));
68+
const browse = sinon.stub(windowApis, 'showOpenDialog').resolves(undefined);
69+
70+
assert.strictEqual(await pickEnvironment([], [], { projects: [] }), undefined);
71+
sinon.assert.calledOnce(browse);
72+
});
73+
});
774

875
/**
976
* Test the logic used in environment pickers to include interpreter paths in descriptions

0 commit comments

Comments
 (0)