From af0ffb809eab782d0802a56e5533e8e40b12b7fa Mon Sep 17 00:00:00 2001 From: Shohruh Ismoilov Date: Thu, 5 Nov 2026 21:17:37 +0500 Subject: [PATCH] fix(project-tree): sort workspace projects by folder name (#1583) Workspace member roots were listed in manifest order. The Project and Project Files views now sort them by the displayed folder name with the same natural, case-insensitive comparison as tree entries; the stored member order is unchanged. Co-authored-by: shohruh-genius <7658451+shohruh-genius@users.noreply.github.com> --- .../project-tree/ProjectView.test.tsx | 22 +++++++++++++++---- src/features/project-tree/model.ts | 6 ++++- src/features/project-tree/roots.test.ts | 5 +++-- src/features/project-tree/store.ts | 12 +++++----- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/features/project-tree/ProjectView.test.tsx b/src/features/project-tree/ProjectView.test.tsx index f3223ccab..2e5880577 100644 --- a/src/features/project-tree/ProjectView.test.tsx +++ b/src/features/project-tree/ProjectView.test.tsx @@ -664,10 +664,24 @@ describe("Show Members", () => { }); }); -it("renders workspace member roots with their own paths, without the settings directory", async () => { - fs = new MemoryFs({ "/z/first.txt": "", "/a/second.txt": "", "/workspace/.openidea/workspace.json": "{}" }); +it("sorts workspace member roots by the folder names shown in the tree", async () => { + fs = new MemoryFs({ + "/a/service10/ten.txt": "", + "/z/Service2/two.txt": "", + "/m/alpha/first.txt": "", + "/workspace/.openidea/workspace.json": "{}", + }); setProjectFsForTests(fs, "/workspace"); - initProjectTree("/workspace", "Workspace", ["/z", "/a"]); + initProjectTree("/workspace", "Workspace", ["/a/service10", "/z/Service2", "/m/alpha"]); const { rows } = setup(); - await waitFor(() => expect(rows()).toEqual(["z/z", "first.txt", "a/a", "second.txt"])); + await waitFor(() => + expect(rows()).toEqual([ + "alpha/m/alpha", + "first.txt", + "Service2/z/Service2", + "two.txt", + "service10/a/service10", + "ten.txt", + ]), + ); }); diff --git a/src/features/project-tree/model.ts b/src/features/project-tree/model.ts index 385868e56..ec9130021 100644 --- a/src/features/project-tree/model.ts +++ b/src/features/project-tree/model.ts @@ -100,6 +100,10 @@ export function isVisible(entry: FileEntry, opts: TreeOptions): boolean { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); +export function compareNames(a: string, b: string): number { + return collator.compare(a, b) || (a < b ? -1 : a > b ? 1 : 0); +} + /** Symlinks report the target's metadata; folders have size 0 (the entry has no target kind). */ export const isDirEntry = (e: FileEntry) => e.kind === "dir" || (e.kind === "symlink" && e.size === 0); @@ -125,7 +129,7 @@ export function sortEntries( const m = (b.mtimeMs ?? 0) - (a.mtimeMs ?? 0); if (m) return m; } - return collator.compare(a.name, b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0); + return compareNames(a.name, b.name); }); } diff --git a/src/features/project-tree/roots.test.ts b/src/features/project-tree/roots.test.ts index ed604c6fd..15646b82b 100644 --- a/src/features/project-tree/roots.test.ts +++ b/src/features/project-tree/roots.test.ts @@ -104,12 +104,13 @@ describe("creating packages", () => { }); }); -it("uses only manifest members in order in workspace Project and Project Files views", () => { +it("shows manifest members in name order in workspace Project and Project Files views", () => { for (const mode of ["project", "projectFiles"] as const) { initProjectTree("/workspace", "Workspace", ["/z", "/a"]); useProjectTree.setState({ mode }); const model = selectModel(useProjectTree.getState()); - expect(model.roots.map((r) => r.path)).toEqual(["/z", "/a"]); + expect(model.roots.map((r) => r.path)).toEqual(["/a", "/z"]); + expect(useProjectTree.getState().members).toEqual(["/z", "/a"]); expect(model.byKey.has("/workspace")).toBe(false); initProjectTree("/workspace", "Workspace", ["/a"]); expect(selectModel(useProjectTree.getState()).roots.map((r) => r.path)).toEqual(["/a"]); diff --git a/src/features/project-tree/store.ts b/src/features/project-tree/store.ts index df53ff6ff..3b51e3495 100644 --- a/src/features/project-tree/store.ts +++ b/src/features/project-tree/store.ts @@ -14,6 +14,7 @@ import { GROUP_PREFIX, buildModel, buildPathsModel, + compareNames, extraFolders, isGroupKey, isLibKey, @@ -34,7 +35,7 @@ import { } from "./model"; import { projectFs } from "./fs"; import { projectViewRoots, useProjectViewRoots, type ProjectViewRootContribution } from "@/workbench/contributions"; -import { ancestorsWithin, isSameOrInside, type PackageKind } from "./paths"; +import { ancestorsWithin, isSameOrInside, nameOf, type PackageKind } from "./paths"; export interface ViewOptions extends TreeOptions { /** Select the active editor's file in the tree. */ @@ -218,12 +219,9 @@ export function selectModel(s: ProjectTreeState): TreeModel { const extras = useProjectViewRoots.getState().roots; const options = treeOptions(s); const contributed = projectViewRoots(extras); - const memberRoots = s.members?.map((path) => ({ - path, - label: path.slice(path.lastIndexOf("/") + 1), - icon: "", - detail: path, - })); + const memberRoots = s.members + ?.map((path) => ({ path, label: nameOf(path), icon: "", detail: path })) + .sort((a, b) => compareNames(a.label, b.label)); const project = () => memberRoots ? withExtraRoots(EMPTY_MODEL, memberRoots, s.listings, options, placeholder)