diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 9fd540ab..2e7a7f85 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -290,12 +290,34 @@ export async function getProjectInstallable( const uniqueResults = Array.from(new Map(results.map((uri) => [uri.fsPath, uri])).values()); const fsPaths = projects.map((p) => p.uri.fsPath); + // Compute depth relative to the owning project root so ordering reflects + // "shallower within the project", independent of where the project lives on disk. + const depthFromProject = (uri: Uri): number => { + const projectRoot = api.getPythonProject(uri)?.uri.fsPath; + if (!projectRoot) { + return Number.MAX_SAFE_INTEGER; + } + const rel = path.relative(projectRoot, uri.fsPath); + if (!rel) { + return 0; + } + return rel.split(path.sep).filter((segment) => segment.length > 0).length; + }; const filtered = uniqueResults .filter((uri) => { const p = api.getPythonProject(uri)?.uri.fsPath; return p && fsPaths.includes(p); }) - .sort(); + .sort((a, b) => { + // Sort by path depth relative to the project root (shallowest first) so + // top-level files like requirements.txt appear before deeply nested ones. + const depthA = depthFromProject(a); + const depthB = depthFromProject(b); + if (depthA !== depthB) { + return depthA - depthB; + } + return a.fsPath.localeCompare(b.fsPath); + }); await Promise.all( filtered.map(async (uri) => { diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts index 5ae58126..f817f6c7 100644 --- a/src/test/managers/builtin/pipUtils.unit.test.ts +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -199,4 +199,39 @@ suite('Pip Utils - getProjectInstallable', () => { assert.ok(firstResult.uri, 'Should have a URI'); assert.ok(firstResult.uri.fsPath.startsWith(workspacePath), 'Should be in workspace directory'); }); + + test('should sort shallower files before deeper ones', async () => { + // Arrange: Use the shared workspacePath from setup() so paths are platform-safe. + const workspacePath = Uri.file('/test/path/root').fsPath; + const rootReqPath = path.join(workspacePath, 'requirements.txt'); + const subdirReqPath = path.join(workspacePath, 'subdir', 'dev-requirements.txt'); + const deepReqPath = path.join(workspacePath, 'deep', 'nested', 'sub', 'requirements.txt'); + + // Return files at different depths, with deeper ones discovered first. + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + return Promise.resolve([Uri.file(deepReqPath), Uri.file(subdirReqPath)]); + } else if (pattern === '*requirements*.txt') { + return Promise.resolve([Uri.file(rootReqPath)]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = (await getProjectInstallable(mockApi as PythonEnvironmentApi, projects)).installables; + + // Assert: order by fsPath so the two `requirements.txt` files are unambiguous. + assert.strictEqual(result.length, 3); + const fsPaths = result.map((r) => r.uri!.fsPath); + assert.deepStrictEqual( + fsPaths, + [rootReqPath, subdirReqPath, deepReqPath], + 'Files should be ordered by depth relative to the project root', + ); + }); });