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 000000000000..d3982352e885 Binary files /dev/null and b/platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.idea.custom/icon.png differ diff --git a/platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.projectidentity b/platform/platform-tests/testData/recentProjects/regularFileIdentityProject/.projectidentity new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/project/impl/RecentProjectsTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/project/impl/RecentProjectsTest.kt index 64e6643ff582..2ecd6bc970d5 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/project/impl/RecentProjectsTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/openapi/project/impl/RecentProjectsTest.kt @@ -3,22 +3,36 @@ package com.intellij.openapi.project.impl +import com.intellij.configurationStore.FileStorageAnnotation +import com.intellij.configurationStore.ProjectStoreDescriptor +import com.intellij.configurationStore.ProjectStorePathCustomizer +import com.intellij.configurationStore.StateStorageManager import com.intellij.ide.AppLifecycleListener import com.intellij.ide.ProjectGroup import com.intellij.ide.ProjectGroupActionGroup import com.intellij.ide.RecentProjectListActionProvider import com.intellij.ide.RecentProjectsManager import com.intellij.ide.RecentProjectsManagerBase +import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.StateStorageOperation +import com.intellij.openapi.components.Storage +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectCloseListener import com.intellij.openapi.project.ex.ProjectManagerEx import com.intellij.project.stateStore +import com.intellij.testFramework.ExtensionTestUtil import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.TemporaryDirectoryExtension import com.intellij.testFramework.assertions.Assertions.assertThat import com.intellij.testFramework.common.timeoutRunBlocking import com.intellij.testFramework.createTestOpenProjectOptions import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.TestDisposable import com.intellij.ui.DeferredIconImpl import com.intellij.ui.JBColor import com.intellij.util.IconUtil @@ -26,12 +40,16 @@ import com.intellij.util.PathUtil import com.intellij.util.messages.SimpleMessageBusConnection import com.intellij.util.ui.AvatarUtils import com.intellij.util.ui.EmptyIcon +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import java.awt.Color +import java.nio.file.Files import java.nio.file.Path +import javax.swing.Icon import kotlin.test.assertEquals @TestApplication @@ -138,25 +156,24 @@ class RecentProjectsTest { val icon = (rpm.getProjectIcon(slnFile.toString(), isProjectValid = true) as DeferredIconImpl<*>).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) +}