From 08fe83d7f500ae4bab1df8929167c0faa74ff6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Baczy=C5=84ski?= Date: Thu, 23 Jul 2026 14:46:54 +0200 Subject: [PATCH] BAZEL-2972: Fix project icon not loaded for projects with regular file identities `RecentProjectIconHelper.getDotIdeaPath` assumes the `./.idea/.idea./.idea` layout for every non-directory project root. This layout seems to be special case for Rider. For other cases standard `ProjectStorePathManager.getInstance().getStoreDescriptor(file).dotIdea` should be used. This issue affected Bazel plugin projects - icon is not shown correctly GitOrigin-RevId: cea7b3f48555ea96325d4dbaceb2d30007c1b499 --- .../intellij/ide/RecentProjectIconHelper.kt | 4 +- .../.idea.custom/icon.png | Bin 0 -> 1430 bytes .../.projectidentity | 0 .../project/impl/RecentProjectsTest.kt | 117 +++++++++++++++--- 4 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.idea.custom/icon.png create mode 100644 platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.projectidentity diff --git a/platform/platform-impl/src/com/intellij/ide/RecentProjectIconHelper.kt b/platform/platform-impl/src/com/intellij/ide/RecentProjectIconHelper.kt index b1d3aaeac714..280dc540bf30 100644 --- a/platform/platform-impl/src/com/intellij/ide/RecentProjectIconHelper.kt +++ b/platform/platform-impl/src/com/intellij/ide/RecentProjectIconHelper.kt @@ -72,7 +72,9 @@ class RecentProjectIconHelper { val fileName = file.fileName.toString() val dotIndex = fileName.lastIndexOf('.') val fileNameWithoutExt = if (dotIndex == -1) fileName else fileName.take(dotIndex) - return file.parent.resolve("$DIRECTORY_STORE_FOLDER/$DIRECTORY_STORE_FOLDER.$fileNameWithoutExt/$DIRECTORY_STORE_FOLDER") + val nestedDotIdea = file.parent.resolve("$DIRECTORY_STORE_FOLDER/$DIRECTORY_STORE_FOLDER.$fileNameWithoutExt/$DIRECTORY_STORE_FOLDER") + if (Files.isDirectory(nestedDotIdea)) return nestedDotIdea + return ProjectStorePathManager.getInstance().getStoreDescriptor(file).dotIdea } catch (_: InvalidPathException) { return null diff --git a/platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.idea.custom/icon.png b/platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.idea.custom/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d3982352e88544448ad9708280221b2f4e1c47e5 GIT binary patch literal 1430 zcmbVMON-M$9FH&9T@eI9L@zNOd~Gt5^s()*QPyU)1+y+|!FqE#Gi`@WGBKHM+dT>% z#804j_a-VH1P?0WQ9Sx7MEnBkq>m`FJ_D0^{pR<7|L?4?T{^IEbV1X!1C6@3q2^QB znA@%XdmaF!run#jJ=L`RhqAFtyZ_*brtLe(n=RS$FC!vC3scc%)-a3}TGN)#4`WO= znbg~?%OltL^x~zVbLtw`N?M8r8A@70J7~kZIF+S#uF;b+MgR;318Y#QMA8LzwOR$x0ghuTgqe;aiHByCo|rLs zEF}q#B^QyNG2*u9OV?1EPD}{noHk0QZc+{dLmUI!f|*HUASF32?kB-mI3<7uEM$>P z71qwN%1geV!*`?&!yKK;)h(sb)V3YbX>$}au)$K%PY7GxQW8(h6pMR8inJ$wVMl(Q zTN9&N!mt!cQ;1*|sP!3(?%0;2pYk!`QO0{ZlQVt7JS>@OsJPiCESqqtX_t^)L5_14 z77&CJsIM}CVu}9_EGX}c{s^T6bwm).evaluateAsync() assertThat(icon).isNotInstanceOf(EmptyIcon::class.java) - // For custom icons we add a 2px empty border - val emptyBorderWidth = 2 - val iconSize = 20 + assertSingleColorIcon(icon, if (JBColor.isBright()) Color.BLUE else Color.RED) + } - // Check that image is loaded from file, and not generated by IDE - val iconImage = IconUtil.toBufferedImage(icon) - for (x in 0 until iconImage.width) { - for (y in 0 until iconImage.height) { - val color = iconImage.getRGB(x, y) - - if (x >= emptyBorderWidth && x < (iconSize - emptyBorderWidth) && - y >= emptyBorderWidth && y < (iconSize - emptyBorderWidth)) { - assertThat(color).isEqualTo(if (JBColor.isBright()) Color.BLUE.rgb else Color.RED.rgb) - } - else { - assertThat(color).isEqualTo(0) - } - } - } + @Test + fun projectIconForRegularFileProjectIdentity(@TestDisposable disposable: Disposable) = timeoutRunBlocking { + JBColor.setDark(false) + ExtensionTestUtil.maskExtensions( + pointName = PROJECT_STORE_PATH_CUSTOMIZER_EP, + newExtensions = listOf(CustomDotIdeaProjectStorePathCustomizer()), + parentDisposable = disposable, + fireEvents = false, + ) + val rpm = (RecentProjectsManager.getInstance() as RecentProjectsManagerBase) + val projectDir = Path.of("${PlatformTestUtil.getPlatformTestDataPath()}/recentProjects/regularFileIdentityProject") + val identityFile = projectDir.resolve(".projectidentity") + val icon = (rpm.getProjectIcon(identityFile.toString(), isProjectValid = true) as DeferredIconImpl<*>).evaluateAsync() + assertThat(icon).isNotInstanceOf(EmptyIcon::class.java) + assertSingleColorIcon(icon, Color.BLUE) } @Test @@ -183,6 +200,27 @@ class RecentProjectsTest { } } + private suspend fun assertSingleColorIcon(icon: Icon, expectedColor: Color) = withContext(Dispatchers.EDT) { + // For custom icons we add a 2px empty border + val emptyBorderWidth = 2 + val iconSize = 20 + // Check that image is loaded from file, and not generated by IDE + val iconImage = IconUtil.toBufferedImage(icon) + for (x in 0 until iconImage.width) { + for (y in 0 until iconImage.height) { + val color = iconImage.getRGB(x, y) + + if (x >= emptyBorderWidth && x < (iconSize - emptyBorderWidth) && + y >= emptyBorderWidth && y < (iconSize - emptyBorderWidth)) { + assertThat(color).isEqualTo(expectedColor.rgb) + } + else { + assertThat(color).isEqualTo(0) + } + } + } + } + private fun getProjectOpenTimestamp(@Suppress("SameParameterValue") projectName: String): Long { val additionalInfo = RecentProjectsManagerBase.getInstanceEx().state.additionalInfo for (s in additionalInfo.keys) { @@ -249,4 +287,45 @@ class RecentProjectsTest { projectManager.forceCloseProjectAsync(project) } } -} \ No newline at end of file +} + +private val PROJECT_STORE_PATH_CUSTOMIZER_EP = ExtensionPointName.create("com.intellij.projectStorePathCustomizer") + +private class CustomDotIdeaProjectStorePathCustomizer : ProjectStorePathCustomizer { + override fun getStoreDirectoryPath(projectRoot: Path): ProjectStoreDescriptor? { + if (projectRoot.fileName?.toString() != ".projectidentity") return null + val projectDir = projectRoot.parent + return CustomDotIdeaProjectStoreDescriptor( + projectIdentityFile = projectRoot, + dotIdea = projectDir.resolve(".idea.custom"), + historicalProjectBasePath = projectDir, + ) + } +} + +private class CustomDotIdeaProjectStoreDescriptor( + override val projectIdentityFile: Path, + override val dotIdea: Path, + override val historicalProjectBasePath: Path, +) : ProjectStoreDescriptor { + + override fun testStoreDirectoryExistsForProjectRoot(): Boolean = Files.isRegularFile(projectIdentityFile) + + override fun getJpsBridgeAwareStorageSpec(filePath: String, project: Project): Storage = + FileStorageAnnotation.PROJECT_FILE_STORAGE_ANNOTATION + + override fun getModuleStorageSpecs( + component: PersistentStateComponent<*>, + stateSpec: State, + operation: StateStorageOperation, + storageManager: StateStorageManager, + project: Project, + ): List = listOf(FileStorageAnnotation.MODULE_FILE_STORAGE_ANNOTATION) + + override fun getStorageSpecs( + component: PersistentStateComponent, + stateSpec: State, + operation: StateStorageOperation, + storageManager: StateStorageManager, + ): List = listOf(FileStorageAnnotation.PROJECT_FILE_STORAGE_ANNOTATION) +}