diff --git a/crates/ide-host/src/ops/build.rs b/crates/ide-host/src/ops/build.rs index 7f775c57e..4732abd50 100644 --- a/crates/ide-host/src/ops/build.rs +++ b/crates/ide-host/src/ops/build.rs @@ -313,10 +313,11 @@ pub async fn build_sync( if tool == BuildTool::Gradle { // The sync may report project folders outside the project (`includeFlat`): index and list them. project.refresh_index_scope(); - project.sink.emit(ide_core::IdeEvent::ProjectRootsChanged { root: project.root.clone().into() }); let p = project.clone(); tokio::task::spawn_blocking(move || p.refresh_repositories()); } + // New source folders and dependency versions: the Project view and External Libraries reload. + project.sink.emit(ide_core::IdeEvent::ProjectRootsChanged { root: project.root.clone().into() }); if tool == BuildTool::Maven && model.maven.is_some() && let Some(bridge) = &project.kotlin_bridge diff --git a/src/features/libraries/index.ts b/src/features/libraries/index.ts index 6480bb650..1cf84d97b 100644 --- a/src/features/libraries/index.ts +++ b/src/features/libraries/index.ts @@ -83,6 +83,14 @@ export function touchesManifest(paths: readonly string[]): boolean { ); } +/** IDEA's order, whatever order the ecosystems were detected in: SDKs first, then libraries, each by name. */ +function sortedGroups(groups: readonly LibraryGroup[]): LibraryGroup[] { + const rank = (g: LibraryGroup) => (SDK_KINDS.has(g.kind) ? 0 : 1); + return [...groups].sort( + (a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true }), + ); +} + /** The "External Libraries" node for `groups` (`< Python 3.12 (.venv) >` › `python3.12`, `site-packages`). */ export function librariesRoot(groups: readonly LibraryGroup[]): ProjectViewRootContribution | null { if (!groups.length) return null; @@ -93,7 +101,7 @@ export function librariesRoot(groups: readonly LibraryGroup[]): ProjectViewRootC // After the project, before Scratches and Consoles (IDEA's order). order: 5, readOnly: true, - children: groups.map((g) => ({ + children: sortedGroups(groups).map((g) => ({ label: SDK_KINDS.has(g.kind) ? `< ${g.name} >` : g.name, icon: GROUP_ICONS[g.kind] ?? "library", children: g.roots.map((r) => ({ @@ -192,12 +200,18 @@ export function activate(): () => void { clearTimeout(timer); timer = setTimeout(refresh, MANIFEST_REFRESH_DELAY); }); + // A Maven / Gradle sync changes the dependency versions without touching a manifest. + const unsubRoots = onIdeEvent("projectRootsChanged", () => { + clearTimeout(timer); + timer = setTimeout(refresh, MANIFEST_REFRESH_DELAY); + }); return () => { seq++; if (request !== undefined) void commands.cancelRequest(request).catch(() => {}); clearTimeout(timer); unsub(); unsubFs(); + unsubRoots(); disposeActions(); clear(); }; diff --git a/src/features/libraries/libraries.test.ts b/src/features/libraries/libraries.test.ts index 756aed17b..cfd166d48 100644 --- a/src/features/libraries/libraries.test.ts +++ b/src/features/libraries/libraries.test.ts @@ -37,18 +37,46 @@ describe("External Libraries", () => { }), ), )!; - expect(root.children?.map((g) => g.icon)).toEqual([ - "file-java", - "file-js", - "file-npm", - "file-rust", - "file-cargo", - "file-go", - "file-gomod", - "maven-project", - "file-gradle", - "library", + expect(Object.fromEntries(root.children!.map((g) => [g.label, g.icon]))).toEqual({ + "< jdk >": "file-java", + nodeCore: "file-js", + nodeModules: "file-npm", + "< rust >": "file-rust", + cargo: "file-cargo", + "< go >": "file-go", + goModules: "file-gomod", + maven: "maven-project", + gradle: "file-gradle", + other: "library", + }); + }); + + it("lists SDKs first, then libraries by name, whatever order they were detected in", () => { + const group = (name: string, kind: string) => ({ name, kind, roots: [{ name: "r", path: `/libs/${name}` }] }); + const detected = [ + group("Maven: org.junit:junit:4.13", "maven"), + group("temurin-21", "jdk"), + group("node_modules", "nodeModules"), + group("Python 3.14", "python"), + group("Cargo dependencies", "cargo"), + group("Gradle: com.acme:lib:1.0", "gradle"), + group("Go SDK 1.27.4", "go"), + group("Maven: com.acme:core:2.1.27", "maven"), + group("Node.js Core", "nodeCore"), + ]; + const labels = (groups: typeof detected) => librariesRoot(groups)!.children!.map((g) => g.label); + expect(labels(detected)).toEqual([ + "< Go SDK 1.27.4 >", + "< Python 3.14 >", + "< temurin-21 >", + "Cargo dependencies", + "Gradle: com.acme:lib:1.0", + "Maven: com.acme:core:2.1.27", + "Maven: org.junit:junit:4.13", + "node_modules", + "Node.js Core", ]); + expect(labels([...detected].reverse())).toEqual(labels(detected)); }); it("lists archives (src.zip, dependency jars) as browsable roots", () => {