Skip to content

Commit f0ce532

Browse files
author
Kartik Raj
authored
Only return local environments from the currently opened workspace folders in environments api (#20108)
Closes #20068 Fixes #20105
1 parent 43c059e commit f0ce532

File tree

5 files changed

+90
-18
lines changed

5 files changed

+90
-18
lines changed

src/client/common/vscodeApis/workspaceApis.ts

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

44
import { ConfigurationScope, workspace, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
5+
import { Resource } from '../types';
56

67
export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined {
78
return workspace.workspaceFolders;
89
}
910

11+
export function getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined {
12+
return uri ? workspace.getWorkspaceFolder(uri) : undefined;
13+
}
14+
1015
export function getWorkspaceFolderPaths(): string[] {
1116
return workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? [];
1217
}

src/client/proposedApi.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { getEnvPath } from './pythonEnvironments/base/info/env';
2525
import { IDiscoveryAPI } from './pythonEnvironments/base/locator';
2626
import { IPythonExecutionFactory } from './common/process/types';
2727
import { traceError, traceVerbose } from './logging';
28-
import { normCasePath } from './common/platform/fs-paths';
28+
import { isParentPath, normCasePath } from './common/platform/fs-paths';
2929
import { sendTelemetryEvent } from './telemetry';
3030
import { EventName } from './telemetry/constants';
3131
import {
@@ -35,7 +35,7 @@ import {
3535
} from './deprecatedProposedApi';
3636
import { DeprecatedProposedAPI } from './deprecatedProposedApiTypes';
3737
import { IEnvironmentVariablesProvider } from './common/variables/types';
38-
import { IWorkspaceService } from './common/application/types';
38+
import { getWorkspaceFolder, getWorkspaceFolders } from './common/vscodeApis/workspaceApis';
3939

4040
type ActiveEnvironmentChangeEvent = {
4141
resource: WorkspaceFolder | undefined;
@@ -102,6 +102,19 @@ function getEnvReference(e: Environment) {
102102
return envClass;
103103
}
104104

105+
function filterUsingVSCodeContext(e: PythonEnvInfo) {
106+
const folders = getWorkspaceFolders();
107+
if (e.searchLocation) {
108+
// Only return local environments that are in the currently opened workspace folders.
109+
const envFolderUri = e.searchLocation;
110+
if (folders) {
111+
return folders.some((folder) => isParentPath(envFolderUri.fsPath, folder.uri.fsPath));
112+
}
113+
return false;
114+
}
115+
return true;
116+
}
117+
105118
export function buildProposedApi(
106119
discoveryApi: IDiscoveryAPI,
107120
serviceContainer: IServiceContainer,
@@ -110,7 +123,6 @@ export function buildProposedApi(
110123
const configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
111124
const disposables = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry);
112125
const extensions = serviceContainer.get<IExtensions>(IExtensions);
113-
const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService);
114126
const envVarsProvider = serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider);
115127
function sendApiTelemetry(apiName: string) {
116128
extensions
@@ -126,6 +138,11 @@ export function buildProposedApi(
126138
}
127139
disposables.push(
128140
discoveryApi.onChanged((e) => {
141+
const env = e.new ?? e.old;
142+
if (!env || !filterUsingVSCodeContext(env)) {
143+
// Filter out environments that are not in the current workspace.
144+
return;
145+
}
129146
if (e.old) {
130147
if (e.new) {
131148
onEnvironmentsChanged.fire({ type: 'update', env: convertEnvInfoAndGetReference(e.new) });
@@ -156,7 +173,7 @@ export function buildProposedApi(
156173
}),
157174
envVarsProvider.onDidEnvironmentVariablesChange((e) => {
158175
onEnvironmentVariablesChanged.fire({
159-
resource: workspaceService.getWorkspaceFolder(e),
176+
resource: getWorkspaceFolder(e),
160177
env: envVarsProvider.getEnvironmentVariablesSync(e),
161178
});
162179
}),
@@ -235,7 +252,10 @@ export function buildProposedApi(
235252
},
236253
get known(): Environment[] {
237254
sendApiTelemetry('known');
238-
return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e));
255+
return discoveryApi
256+
.getEnvs()
257+
.filter((e) => filterUsingVSCodeContext(e))
258+
.map((e) => convertEnvInfoAndGetReference(e));
239259
},
240260
async refreshEnvironments(options?: RefreshOptions) {
241261
await discoveryApi.triggerRefresh(undefined, {
@@ -280,7 +300,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment
280300
type: convertEnvType(env.type),
281301
name: env.name === '' ? undefined : env.name,
282302
folderUri: Uri.file(env.location),
283-
workspaceFolder: env.searchLocation,
303+
workspaceFolder: getWorkspaceFolder(env.searchLocation),
284304
}
285305
: undefined,
286306
version: env.executable.filename === 'python' ? undefined : (version as ResolvedEnvironment['version']),

src/client/proposedApiTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface ProposedExtensionAPI {
3232
/**
3333
* Carries environments known to the extension at the time of fetching the property. Note this may not
3434
* contain all environments in the system as a refresh might be going on.
35+
*
36+
* Only reports environments in the current workspace.
3537
*/
3638
readonly known: readonly Environment[];
3739
/**
@@ -125,7 +127,7 @@ export type Environment = EnvironmentPath & {
125127
/**
126128
* Any specific workspace folder this environment is created for.
127129
*/
128-
readonly workspaceFolder: Uri | undefined;
130+
readonly workspaceFolder: WorkspaceFolder | undefined;
129131
}
130132
| undefined;
131133
/**

src/client/pythonEnvironments/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import * as vscode from 'vscode';
55
import { Uri } from 'vscode';
6+
import { cloneDeep } from 'lodash';
67
import { getGlobalStorage, IPersistentStorage } from '../common/persistentState';
78
import { getOSType, OSType } from '../common/utils/platform';
89
import { ActivationResult, ExtensionState } from '../components';
@@ -34,6 +35,7 @@ import {
3435
} from './base/locators/composite/envsCollectionCache';
3536
import { EnvsCollectionService } from './base/locators/composite/envsCollectionService';
3637
import { IDisposable } from '../common/types';
38+
import { traceError } from '../logging';
3739

3840
/**
3941
* Set up the Python environments component (during extension activation).'
@@ -192,6 +194,8 @@ function getFromStorage(storage: IPersistentStorage<PythonEnvInfo[]>): PythonEnv
192194
e.searchLocation = Uri.parse(e.searchLocation);
193195
} else if ('scheme' in e.searchLocation && 'path' in e.searchLocation) {
194196
e.searchLocation = Uri.parse(`${e.searchLocation.scheme}://${e.searchLocation.path}`);
197+
} else {
198+
traceError('Unexpected search location', JSON.stringify(e.searchLocation));
195199
}
196200
}
197201
return e;
@@ -200,7 +204,8 @@ function getFromStorage(storage: IPersistentStorage<PythonEnvInfo[]>): PythonEnv
200204

201205
function putIntoStorage(storage: IPersistentStorage<PythonEnvInfo[]>, envs: PythonEnvInfo[]): Promise<void> {
202206
storage.set(
203-
envs.map((e) => {
207+
// We have to `cloneDeep()` here so that we don't overwrite the original `PythonEnvInfo` objects.
208+
cloneDeep(envs).map((e) => {
204209
if (e.searchLocation) {
205210
// Make TS believe it is string. This is temporary. We need to serialize this in
206211
// a custom way.

src/test/proposedApi.unit.test.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import * as typemoq from 'typemoq';
5+
import * as sinon from 'sinon';
56
import { assert, expect } from 'chai';
67
import { Uri, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode';
78
import { cloneDeep } from 'lodash';
@@ -11,6 +12,7 @@ import {
1112
IExtensions,
1213
IInterpreterPathService,
1314
IPythonSettings,
15+
Resource,
1416
} from '../client/common/types';
1517
import { IServiceContainer } from '../client/ioc/types';
1618
import {
@@ -35,8 +37,15 @@ import {
3537
} from '../client/proposedApiTypes';
3638
import { IWorkspaceService } from '../client/common/application/types';
3739
import { IEnvironmentVariablesProvider } from '../client/common/variables/types';
40+
import * as workspaceApis from '../client/common/vscodeApis/workspaceApis';
3841

3942
suite('Proposed Extension API', () => {
43+
const workspacePath = 'path/to/workspace';
44+
const workspaceFolder = {
45+
name: 'workspace',
46+
uri: Uri.file(workspacePath),
47+
index: 0,
48+
};
4049
let serviceContainer: typemoq.IMock<IServiceContainer>;
4150
let discoverAPI: typemoq.IMock<IDiscoveryAPI>;
4251
let interpreterPathService: typemoq.IMock<IInterpreterPathService>;
@@ -52,6 +61,13 @@ suite('Proposed Extension API', () => {
5261

5362
setup(() => {
5463
serviceContainer = typemoq.Mock.ofType<IServiceContainer>();
64+
sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]);
65+
sinon.stub(workspaceApis, 'getWorkspaceFolder').callsFake((resource: Resource) => {
66+
if (resource?.fsPath === workspaceFolder.uri.fsPath) {
67+
return workspaceFolder;
68+
}
69+
return undefined;
70+
});
5571
discoverAPI = typemoq.Mock.ofType<IDiscoveryAPI>();
5672
extensions = typemoq.Mock.ofType<IExtensions>();
5773
workspaceService = typemoq.Mock.ofType<IWorkspaceService>();
@@ -85,21 +101,20 @@ suite('Proposed Extension API', () => {
85101
teardown(() => {
86102
// Verify each API method sends telemetry regarding who called the API.
87103
extensions.verifyAll();
104+
sinon.restore();
88105
});
89106

90107
test('Provide an event to track when environment variables change', async () => {
91-
const resource = Uri.file('x');
92-
const folder = ({ uri: resource } as unknown) as WorkspaceFolder;
108+
const resource = workspaceFolder.uri;
93109
const envVars = { PATH: 'path' };
94110
envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars);
95-
workspaceService.setup((w) => w.getWorkspaceFolder(resource)).returns(() => folder);
96111
const events: EnvironmentVariablesChangeEvent[] = [];
97112
proposed.environments.onDidEnvironmentVariablesChange((e) => {
98113
events.push(e);
99114
});
100115
onDidChangeEnvironmentVariables.fire(resource);
101116
await sleep(1);
102-
assert.deepEqual(events, [{ env: envVars, resource: folder }]);
117+
assert.deepEqual(events, [{ env: envVars, resource: workspaceFolder }]);
103118
});
104119

105120
test('getEnvironmentVariables: No resource', async () => {
@@ -196,7 +211,7 @@ suite('Proposed Extension API', () => {
196211
kind: PythonEnvKind.System,
197212
arch: Architecture.x64,
198213
sysPrefix: 'prefix/path',
199-
searchLocation: Uri.file('path/to/project'),
214+
searchLocation: Uri.file(workspacePath),
200215
});
201216
discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env));
202217

@@ -216,13 +231,13 @@ suite('Proposed Extension API', () => {
216231
kind: PythonEnvKind.System,
217232
arch: Architecture.x64,
218233
sysPrefix: 'prefix/path',
219-
searchLocation: Uri.file('path/to/project'),
234+
searchLocation: Uri.file(workspacePath),
220235
});
221236
const partialEnv = buildEnvInfo({
222237
executable: pythonPath,
223238
kind: PythonEnvKind.System,
224239
sysPrefix: 'prefix/path',
225-
searchLocation: Uri.file('path/to/project'),
240+
searchLocation: Uri.file(workspacePath),
226241
});
227242
discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env));
228243

@@ -237,7 +252,7 @@ suite('Proposed Extension API', () => {
237252
});
238253

239254
test('environments: python found', async () => {
240-
const envs = [
255+
const expectedEnvs = [
241256
{
242257
executable: {
243258
filename: 'this/is/a/test/python/path1',
@@ -281,12 +296,37 @@ suite('Proposed Extension API', () => {
281296
},
282297
},
283298
];
299+
const envs = [
300+
...expectedEnvs,
301+
{
302+
executable: {
303+
filename: 'this/is/a/test/python/path3',
304+
ctime: 1,
305+
mtime: 2,
306+
sysPrefix: 'prefix/path',
307+
},
308+
version: {
309+
major: 3,
310+
minor: -1,
311+
micro: -1,
312+
},
313+
kind: PythonEnvKind.Venv,
314+
arch: Architecture.x64,
315+
name: '',
316+
location: '',
317+
source: [PythonEnvSource.PathEnvVar],
318+
distro: {
319+
org: '',
320+
},
321+
searchLocation: Uri.file('path/outside/workspace'),
322+
},
323+
];
284324
discoverAPI.setup((d) => d.getEnvs()).returns(() => envs);
285325
const actual = proposed.environments.known;
286326
const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal);
287327
assert.deepEqual(
288328
actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)),
289-
envs.map((e) => convertEnvInfo(e)).sort((a, b) => a.id.localeCompare(b.id)),
329+
expectedEnvs.map((e) => convertEnvInfo(e)).sort((a, b) => a.id.localeCompare(b.id)),
290330
);
291331
});
292332

@@ -302,7 +342,7 @@ suite('Proposed Extension API', () => {
302342
executable: 'pythonPath',
303343
kind: PythonEnvKind.System,
304344
sysPrefix: 'prefix/path',
305-
searchLocation: Uri.file('path/to/project'),
345+
searchLocation: Uri.file(workspacePath),
306346
}),
307347
{
308348
executable: {

0 commit comments

Comments
 (0)