fix(libraries): keep External Libraries in a stable order and reload after a build sync (#3077)

External Libraries groups were listed in detection order, which differed between the streamed and final results; they are now sorted SDKs first, then libraries by name. A Maven sync that changed dependency versions without touching a manifest left old versions listed; every successful build sync now emits projectRootsChanged (Gradle already did) and External Libraries reloads on it.
This commit is contained in:
l0ng-ai
2026-11-13 13:48:12 +08:00
committed by GitHub
parent 86f7b747a5
commit cd79293fd7
3 changed files with 56 additions and 13 deletions
+2 -1
View File
@@ -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
+15 -1
View File
@@ -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();
};
+39 -11
View File
@@ -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", () => {