Skip to content

Commit 08fbd27

Browse files
eleanorjboydCopilotdependabot[bot]
authored
implement MVP of workspace and global searchPaths (#863)
Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 01a0b46 commit 08fbd27

File tree

5 files changed

+638
-7
lines changed

5 files changed

+638
-7
lines changed

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,24 @@
109109
"description": "%python-envs.terminal.useEnvFile.description%",
110110
"default": false,
111111
"scope": "resource"
112+
},
113+
"python-env.globalSearchPaths": {
114+
"type": "array",
115+
"description": "%python-env.globalSearchPaths.description%",
116+
"default": [],
117+
"scope": "machine",
118+
"items": {
119+
"type": "string"
120+
}
121+
},
122+
"python-env.workspaceSearchPaths": {
123+
"type": "array",
124+
"description": "%python-env.workspaceSearchPaths.description%",
125+
"default": [],
126+
"scope": "resource",
127+
"items": {
128+
"type": "string"
129+
}
112130
}
113131
}
114132
},

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"python-envs.terminal.autoActivationType.shellStartup": "Activation by modifying the terminal shell startup script. To use this feature we will need to modify your shell startup scripts.",
1212
"python-envs.terminal.autoActivationType.off": "No automatic activation of environments.",
1313
"python-envs.terminal.useEnvFile.description": "Controls whether environment variables from .env files and python.envFile setting are injected into terminals.",
14+
"python-env.globalSearchPaths.description": "Global search paths for Python environments. Absolute directory paths that are searched at the user level.",
15+
"python-env.workspaceSearchPaths.description": "Workspace search paths for Python environments. Can be absolute paths or relative directory paths searched within the workspace.",
1416
"python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes",
1517
"python-envs.reportIssue.title": "Report Issue",
1618
"python-envs.setEnvManager.title": "Set Environment Manager",

src/common/utils/pathUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,12 @@ export function untildify(path: string): string {
9393
export function getUserHomeDir(): string {
9494
return os.homedir();
9595
}
96+
97+
/**
98+
* Applies untildify to an array of paths
99+
* @param paths Array of potentially tilde-containing paths
100+
* @returns Array of expanded paths
101+
*/
102+
export function untildifyArray(paths: string[]): string[] {
103+
return paths.map((p) => untildify(p));
104+
}

src/managers/common/nativePythonFinder.ts

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import * as rpc from 'vscode-jsonrpc/node';
77
import { PythonProjectApi } from '../../api';
88
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants';
99
import { getExtension } from '../../common/extension.apis';
10-
import { traceVerbose } from '../../common/logging';
11-
import { untildify } from '../../common/utils/pathUtils';
10+
import { traceError, traceLog, traceVerbose, traceWarn } from '../../common/logging';
11+
import { untildify, untildifyArray } from '../../common/utils/pathUtils';
1212
import { isWindows } from '../../common/utils/platformUtils';
1313
import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool';
14-
import { getConfiguration } from '../../common/workspace.apis';
14+
import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis';
1515
import { noop } from './utils';
1616

1717
export async function getNativePythonToolsPath(): Promise<string> {
@@ -326,10 +326,12 @@ class NativePythonFinderImpl implements NativePythonFinder {
326326
* Must be invoked when ever there are changes to any data related to the configuration details.
327327
*/
328328
private async configure() {
329+
// Get all extra search paths including legacy settings and new searchPaths
330+
const extraSearchPaths = await getAllExtraSearchPaths();
331+
329332
const options: ConfigurationOptions = {
330333
workspaceDirectories: this.api.getPythonProjects().map((item) => item.uri.fsPath),
331-
// We do not want to mix this with `search_paths`
332-
environmentDirectories: getCustomVirtualEnvDirs(),
334+
environmentDirectories: extraSearchPaths,
333335
condaExecutable: getPythonSettingAndUntildify<string>('condaPath'),
334336
poetryExecutable: getPythonSettingAndUntildify<string>('poetryPath'),
335337
cacheDirectory: this.cacheDirectory?.fsPath,
@@ -357,9 +359,9 @@ type ConfigurationOptions = {
357359
cacheDirectory?: string;
358360
};
359361
/**
360-
* Gets all custom virtual environment locations to look for environments.
362+
* Gets all custom virtual environment locations to look for environments from the legacy python settings (venvPath, venvFolders).
361363
*/
362-
function getCustomVirtualEnvDirs(): string[] {
364+
function getCustomVirtualEnvDirsLegacy(): string[] {
363365
const venvDirs: string[] = [];
364366
const venvPath = getPythonSettingAndUntildify<string>('venvPath');
365367
if (venvPath) {
@@ -380,6 +382,109 @@ function getPythonSettingAndUntildify<T>(name: string, scope?: Uri): T | undefin
380382
return value;
381383
}
382384

385+
/**
386+
* Gets all extra environment search paths from various configuration sources.
387+
* Combines legacy python settings (with migration), globalSearchPaths, and workspaceSearchPaths.
388+
* @returns Array of search directory paths
389+
*/
390+
export async function getAllExtraSearchPaths(): Promise<string[]> {
391+
const searchDirectories: string[] = [];
392+
393+
// add legacy custom venv directories
394+
const customVenvDirs = getCustomVirtualEnvDirsLegacy();
395+
searchDirectories.push(...customVenvDirs);
396+
397+
// Get globalSearchPaths
398+
const globalSearchPaths = getGlobalSearchPaths().filter((path) => path && path.trim() !== '');
399+
searchDirectories.push(...globalSearchPaths);
400+
401+
// Get workspaceSearchPaths
402+
const workspaceSearchPaths = getWorkspaceSearchPaths();
403+
404+
// Resolve relative paths against workspace folders
405+
for (const searchPath of workspaceSearchPaths) {
406+
if (!searchPath || searchPath.trim() === '') {
407+
continue;
408+
}
409+
410+
const trimmedPath = searchPath.trim();
411+
412+
if (path.isAbsolute(trimmedPath)) {
413+
// Absolute path - use as is
414+
searchDirectories.push(trimmedPath);
415+
} else {
416+
// Relative path - resolve against all workspace folders
417+
const workspaceFolders = getWorkspaceFolders();
418+
if (workspaceFolders) {
419+
for (const workspaceFolder of workspaceFolders) {
420+
const resolvedPath = path.resolve(workspaceFolder.uri.fsPath, trimmedPath);
421+
searchDirectories.push(resolvedPath);
422+
}
423+
} else {
424+
traceWarn('Warning: No workspace folders found for relative path:', trimmedPath);
425+
}
426+
}
427+
}
428+
429+
// Remove duplicates and return
430+
const uniquePaths = Array.from(new Set(searchDirectories));
431+
traceLog(
432+
'getAllExtraSearchPaths completed. Total unique search directories:',
433+
uniquePaths.length,
434+
'Paths:',
435+
uniquePaths,
436+
);
437+
return uniquePaths;
438+
}
439+
440+
/**
441+
* Gets globalSearchPaths setting with proper validation.
442+
* Only gets user-level (global) setting since this setting is application-scoped.
443+
*/
444+
function getGlobalSearchPaths(): string[] {
445+
try {
446+
const envConfig = getConfiguration('python-env');
447+
const inspection = envConfig.inspect<string[]>('globalSearchPaths');
448+
449+
const globalPaths = inspection?.globalValue || [];
450+
return untildifyArray(globalPaths);
451+
} catch (error) {
452+
traceError('Error getting globalSearchPaths:', error);
453+
return [];
454+
}
455+
}
456+
457+
/**
458+
* Gets the most specific workspace-level setting available for workspaceSearchPaths.
459+
*/
460+
function getWorkspaceSearchPaths(): string[] {
461+
try {
462+
const envConfig = getConfiguration('python-env');
463+
const inspection = envConfig.inspect<string[]>('workspaceSearchPaths');
464+
465+
if (inspection?.globalValue) {
466+
traceError(
467+
'Error: python-env.workspaceSearchPaths is set at the user/global level, but this setting can only be set at the workspace or workspace folder level.',
468+
);
469+
}
470+
471+
// For workspace settings, prefer workspaceFolder > workspace
472+
if (inspection?.workspaceFolderValue) {
473+
return inspection.workspaceFolderValue;
474+
}
475+
476+
if (inspection?.workspaceValue) {
477+
return inspection.workspaceValue;
478+
}
479+
480+
// Default empty array (don't use global value for workspace settings)
481+
return [];
482+
} catch (error) {
483+
traceError('Error getting workspaceSearchPaths:', error);
484+
return [];
485+
}
486+
}
487+
383488
export function getCacheDirectory(context: ExtensionContext): Uri {
384489
return Uri.joinPath(context.globalStorageUri, 'pythonLocator');
385490
}

0 commit comments

Comments
 (0)