diff --git a/src/features/interpreterSelection.ts b/src/features/interpreterSelection.ts index d761d4065..0116cc869 100644 --- a/src/features/interpreterSelection.ts +++ b/src/features/interpreterSelection.ts @@ -587,7 +587,8 @@ async function toAbsoluteInterpreterPath(interpreterPath: string, scope: Uri | u if (path.isAbsolute(interpreterPath)) { return interpreterPath; } - if (!/[\\/]/.test(interpreterPath)) { + const isBareCommand = !/[\\/]/.test(interpreterPath); + if (isBareCommand) { const onPath = await findCommandOnPath(interpreterPath); if (onPath) { return onPath; @@ -596,6 +597,24 @@ async function toAbsoluteInterpreterPath(interpreterPath: string, scope: Uri | u if (!scope) { return interpreterPath; } + if (isBareCommand) { + // Keep bare commands intact when they are absent from the extension host's PATH. + // The native finder can resolve them using its own environment (for example, the + // integrated terminal's shell environment). + const workspaceCandidate = path.resolve( + getWorkspaceFolder(scope)?.uri.fsPath ?? scope.fsPath, + interpreterPath, + ); + try { + if ((await fs.stat(workspaceCandidate)).isDirectory()) { + return workspaceCandidate; + } + } catch { + // A missing bare name may still be resolvable by the native finder (for example, by + // an integrated-terminal environment manager), so pass it through unchanged. + } + return interpreterPath; + } const workspaceFolder = getWorkspaceFolder(scope); return path.resolve(workspaceFolder?.uri.fsPath ?? scope.fsPath, interpreterPath); } @@ -666,6 +685,7 @@ async function tryResolveInterpreterPath( } if (manager && resolvedEnv) { + const isBareCommand = !/[\\/]/.test(interpreterPath); // Create a wrapper environment that uses the user's specified path, named like the // resolved environment so the status bar and views show a normal interpreter label. const newEnv: PythonEnvironment = { @@ -677,11 +697,11 @@ async function tryResolveInterpreterPath( displayName: resolvedEnv.displayName, version: resolved.version ?? '', displayPath: interpreterPath, - environmentPath: Uri.file(interpreterPath), + environmentPath: isBareCommand ? resolvedEnv.environmentPath : Uri.file(interpreterPath), sysPrefix: resolved.prefix ?? '', execInfo: { run: { - executable: interpreterPath, + executable: isBareCommand ? resolved.executable : interpreterPath, }, }, }; diff --git a/src/test/features/interpreterSelection.unit.test.ts b/src/test/features/interpreterSelection.unit.test.ts index d0b9ee0ae..7411ecfd6 100644 --- a/src/test/features/interpreterSelection.unit.test.ts +++ b/src/test/features/interpreterSelection.unit.test.ts @@ -488,14 +488,16 @@ suite('Interpreter Selection - Priority Chain', () => { }); } - test('should resolve a command name that is not on PATH against the workspace folder', async () => { + test('should pass a command name that is not on PATH to the native finder unchanged', async () => { const workspaceUri = Uri.file(path.resolve('test', 'workspace')); const workspaceFolder = { name: 'workspace', uri: workspaceUri } as WorkspaceFolder; - const expected = path.resolve(workspaceUri.fsPath, 'pyenvs-test-missing-python'); + const command = 'pyenvs-test-missing-python'; + originalPath = process.env.PATH; + process.env.PATH = ''; sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); - useDefaultInterpreterPath('pyenvs-test-missing-python'); + useDefaultInterpreterPath(command); mockNativeFinder.resolve.rejects(new Error('not found')); mockVenvManager.get.resolves(undefined); @@ -507,7 +509,34 @@ suite('Interpreter Selection - Priority Chain', () => { mockApi as unknown as PythonEnvironmentApi, ); - assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(expected)); + assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(command)); + }); + + test('should resolve a bare workspace directory to its absolute path', async () => { + const workspaceUri = Uri.file(await fs.mkdtemp(path.join(os.tmpdir(), 'dip-workspace-'))); + const workspaceFolder = { name: 'workspace', uri: workspaceUri } as WorkspaceFolder; + const envDirectory = path.join(workspaceUri.fsPath, '.venv'); + await fs.mkdir(envDirectory); + sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration); + sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder); + sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + useDefaultInterpreterPath('.venv'); + mockNativeFinder.resolve.rejects(new Error('not found')); + mockVenvManager.get.resolves(undefined); + + try { + await resolveEnvironmentByPriority( + workspaceUri, + mockEnvManagers as unknown as EnvironmentManagers, + mockProjectManager as unknown as PythonProjectManager, + mockNativeFinder as unknown as NativePythonFinder, + mockApi as unknown as PythonEnvironmentApi, + ); + + assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(envDirectory)); + } finally { + await fs.remove(workspaceUri.fsPath); + } }); test('should treat the default value "python" as not configured', async () => {