diff --git a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/GitDataHoldersInitializer.kt b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/GitDataHoldersInitializer.kt index c8e7edf59309..150198cf32c9 100644 --- a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/GitDataHoldersInitializer.kt +++ b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/GitDataHoldersInitializer.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.startup.ProjectActivity import com.intellij.util.application import com.intellij.vcs.git.shared.branch.GitInOutStateHolder import com.intellij.vcs.git.shared.repo.GitRepositoriesHolder +import com.intellij.vcs.git.shared.repo.GitRepositoryColorsHolder import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -20,5 +21,6 @@ internal class GitDataHoldersInitializer : ProjectActivity { } } GitInOutStateHolder.getInstance(project) + GitRepositoryColorsHolder.getInstance(project) } } diff --git a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/branch/tree/GitBranchesTreeRenderer.kt b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/branch/tree/GitBranchesTreeRenderer.kt index ead393b6dbc6..63b0dbbc5d55 100644 --- a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/branch/tree/GitBranchesTreeRenderer.kt +++ b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/branch/tree/GitBranchesTreeRenderer.kt @@ -76,7 +76,7 @@ abstract class GitBranchesTreeRenderer( val value = treeNode ?: return null return when (value) { is PopupFactoryImpl.ActionItem -> value.getIcon(isSelected) - is GitBranchesTreeModel.RepositoryNode -> GitBranchesTreeIconProvider.forRepository(repoColorGenerator, value.repository.repositoryId) + is GitBranchesTreeModel.RepositoryNode -> GitBranchesTreeIconProvider.forRepository(treePopupStep.project, value.repository.repositoryId) else -> null } } diff --git a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/repo/GitRepositoryColorsHolder.kt b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/repo/GitRepositoryColorsHolder.kt new file mode 100644 index 000000000000..447af5c22d2b --- /dev/null +++ b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/repo/GitRepositoryColorsHolder.kt @@ -0,0 +1,67 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.vcs.git.shared.repo + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.platform.project.projectId +import com.intellij.platform.vcs.impl.shared.rpc.RepositoryId +import com.intellij.vcs.git.shared.repo.GitRepositoryColor.Companion.toAwtColor +import com.intellij.vcs.git.shared.rpc.GitRepositoryColorsApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +@ApiStatus.Internal +@Serializable +data class GitRepositoryColorsState( + val colors: Map = emptyMap(), +) + +/** + * Bring both bright and dark colors not to handle LaF changes + */ +@JvmInline +@ApiStatus.Internal +@Serializable +value class GitRepositoryColor(val rgb: Int) { + companion object { + fun of(color: Color): GitRepositoryColor = GitRepositoryColor(color.rgb) + + fun GitRepositoryColor.toAwtColor(): Color = Color(rgb) + } +} + +@Service(Service.Level.PROJECT) +internal class GitRepositoryColorsHolder(project: Project, cs: CoroutineScope) { + private var state = GitRepositoryColorsState() + + init { + cs.launch { + GitRepositoryColorsApi.getInstance().syncColors(project.projectId()).collect { newState -> + LOG.debug("Received new colors - ${newState.colors.size} repos") + state = newState + } + } + } + + fun getColor(repositoryId: RepositoryId): Color? { + val repoColor = state.colors[repositoryId] + + if (repoColor == null) { + LOG.warn("Color for $repositoryId not loaded") + return null + } + + return repoColor.toAwtColor() + } + + companion object { + private val LOG = Logger.getInstance(GitRepositoryColorsHolder::class.java) + + fun getInstance(project: Project): GitRepositoryColorsHolder = project.service() + } +} \ No newline at end of file diff --git a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/rpc/GitRepositoryColorsApi.kt b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/rpc/GitRepositoryColorsApi.kt new file mode 100644 index 000000000000..1641eca905b3 --- /dev/null +++ b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/rpc/GitRepositoryColorsApi.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.vcs.git.shared.rpc + +import com.intellij.platform.project.ProjectId +import com.intellij.platform.rpc.RemoteApiProviderService +import com.intellij.vcs.git.shared.repo.GitRepositoryColorsState +import fleet.rpc.RemoteApi +import fleet.rpc.Rpc +import fleet.rpc.remoteApiDescriptor +import kotlinx.coroutines.flow.Flow +import org.jetbrains.annotations.ApiStatus + +@Rpc +@ApiStatus.Internal +interface GitRepositoryColorsApi: RemoteApi { + suspend fun syncColors(projectId: ProjectId) : Flow + + companion object { + suspend fun getInstance() : GitRepositoryColorsApi = + RemoteApiProviderService.resolve(remoteApiDescriptor()) + } +} \ No newline at end of file diff --git a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/ui/GitBranchesTreeIconProvider.kt b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/ui/GitBranchesTreeIconProvider.kt index 8c6da75fdacd..72a28a9a1532 100644 --- a/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/ui/GitBranchesTreeIconProvider.kt +++ b/plugins/git4idea/shared/src/com/intellij/vcs/git/shared/ui/GitBranchesTreeIconProvider.kt @@ -4,9 +4,9 @@ package com.intellij.vcs.git.shared.ui import com.intellij.icons.AllIcons import com.intellij.openapi.project.Project import com.intellij.platform.vcs.impl.shared.rpc.RepositoryId -import com.intellij.platform.vcs.impl.shared.ui.RepositoryColorGenerator import com.intellij.util.PlatformIcons import com.intellij.util.ui.CheckboxIcon +import com.intellij.vcs.git.shared.repo.GitRepositoryColorsHolder import git4idea.GitReference import git4idea.GitTag import icons.DvcsImplIcons @@ -24,10 +24,9 @@ object GitBranchesTreeIconProvider { else -> AllIcons.Vcs.BranchNode } - fun forRepository(project: Project, repositoryId: RepositoryId): Icon = GitRepoIconProvider.getIcon(project, repositoryId) - - fun forRepository(repositoryColorGenerator: RepositoryColorGenerator, repositoryId: RepositoryId): Icon { - val color = repositoryColorGenerator.getColor(repositoryId) + fun forRepository(project: Project, repositoryId: RepositoryId): Icon { + val color = GitRepositoryColorsHolder.getInstance(project).getColor(repositoryId) + if (color == null) return GitRepoIconProvider.getIcon(project, repositoryId) return CheckboxIcon.createAndScale(color) } diff --git a/plugins/git4idea/src/git4idea/branch/GitNewBranchDialog.kt b/plugins/git4idea/src/git4idea/branch/GitNewBranchDialog.kt index 1982d04c9077..07cec3bfc4e5 100644 --- a/plugins/git4idea/src/git4idea/branch/GitNewBranchDialog.kt +++ b/plugins/git4idea/src/git4idea/branch/GitNewBranchDialog.kt @@ -20,7 +20,6 @@ import com.intellij.openapi.ui.validation.WHEN_DOCUMENT_CHANGED import com.intellij.openapi.ui.validation.WHEN_STATE_CHANGED import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.text.HtmlBuilder -import com.intellij.platform.vcs.impl.shared.ui.RepositoryColorGeneratorFactory import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.EditorTextField import com.intellij.ui.dsl.builder.* @@ -189,7 +188,6 @@ internal class GitNewBranchDialog @JvmOverloads constructor( private fun createRepositoriesCombobox(): ComboBox { val items = listOf(ALL_REPOSITORIES, *allRepositories.toTypedArray()) - val colorGenerator = RepositoryColorGeneratorFactory.create(allRepositories.map { it.rpcId }) return ComboBox(CollectionComboBoxModel(items)).apply { renderer = listCellRenderer(GitBundle.message("new.branch.dialog.branch.root.all.name")) { @@ -198,7 +196,7 @@ internal class GitNewBranchDialog @JvmOverloads constructor( icon(AllIcons.Empty) } else if (repo != null) { - icon(GitBranchesTreeIconProvider.forRepository(colorGenerator, repo.rpcId)) + icon(GitBranchesTreeIconProvider.forRepository(project, repo.rpcId)) text(DvcsUtil.getShortRepositoryName(repo)) } } diff --git a/plugins/git4idea/src/git4idea/remoteApi/GitApiProvider.kt b/plugins/git4idea/src/git4idea/remoteApi/GitApiProvider.kt index 52b4e5bbfe4b..ae346cb650c1 100644 --- a/plugins/git4idea/src/git4idea/remoteApi/GitApiProvider.kt +++ b/plugins/git4idea/src/git4idea/remoteApi/GitApiProvider.kt @@ -4,10 +4,7 @@ package git4idea.remoteApi import com.intellij.openapi.client.currentSession import com.intellij.platform.rpc.backend.RemoteApiProvider import com.intellij.util.application -import com.intellij.vcs.git.shared.rpc.GitIncomingOutgoingStateApi -import com.intellij.vcs.git.shared.rpc.GitRepositoryApi -import com.intellij.vcs.git.shared.rpc.GitUiSettingsApi -import com.intellij.vcs.git.shared.rpc.GitWidgetApi +import com.intellij.vcs.git.shared.rpc.* import fleet.rpc.remoteApiDescriptor internal class GitApiProvider : RemoteApiProvider { @@ -21,6 +18,9 @@ internal class GitApiProvider : RemoteApiProvider { remoteApi(remoteApiDescriptor()) { GitIncomingOutgoingStateApiImpl() } + remoteApi(remoteApiDescriptor()) { + GitRepositoryColorsApiImpl() + } remoteApi(remoteApiDescriptor()) { GitUiSettingsApiImpl() } diff --git a/plugins/git4idea/src/git4idea/remoteApi/GitRepositoryColorsApiImpl.kt b/plugins/git4idea/src/git4idea/remoteApi/GitRepositoryColorsApiImpl.kt new file mode 100644 index 000000000000..21476e6347e9 --- /dev/null +++ b/plugins/git4idea/src/git4idea/remoteApi/GitRepositoryColorsApiImpl.kt @@ -0,0 +1,88 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.remoteApi + +import com.intellij.dvcs.repo.VcsRepositoryManager +import com.intellij.dvcs.repo.VcsRepositoryMappingListener +import com.intellij.dvcs.ui.RepositoryChangesBrowserNode +import com.intellij.ide.ui.LafManagerListener +import com.intellij.openapi.application.readAction +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.platform.project.ProjectId +import com.intellij.platform.project.findProjectOrNull +import com.intellij.platform.vcs.impl.shared.rpc.RepositoryId +import com.intellij.vcs.git.shared.repo.GitRepositoryColor +import com.intellij.vcs.git.shared.repo.GitRepositoryColorsState +import com.intellij.vcs.git.shared.rpc.GitRepositoryColorsApi +import git4idea.GitDisposable +import git4idea.repo.GitRepositoryManager +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds + + +internal class GitRepositoryColorsApiImpl : GitRepositoryColorsApi { + + @OptIn(FlowPreview::class) + override suspend fun syncColors(projectId: ProjectId): Flow { + requireOwner() + + val project = projectId.findProjectOrNull() ?: return emptyFlow() + + return callbackFlow { + val notifier = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + val messageBusConnection = readAction { + if (project.isDisposed) { + close() + return@readAction null + } + + val coroutineScope = GitDisposable.getInstance(project).coroutineScope + coroutineScope.launch { + send(calcColorsState(project)) + notifier.debounce(100.milliseconds).collectLatest { + LOG.debug("Sending new colors") + send(calcColorsState(project)) + } + } + + project.messageBus.connect(coroutineScope).also { + it.subscribe(VcsRepositoryManager.VCS_REPOSITORY_MAPPING_UPDATED, VcsRepositoryMappingListener { + LOG.debug("VCS mapping changed") + notifier.tryEmit(Unit) + }) + it.subscribe(LafManagerListener.TOPIC, LafManagerListener { + LOG.debug("LAF changed") + notifier.tryEmit(Unit) + }) + } + } + awaitClose { + LOG.debug("Connection closed") + messageBusConnection?.disconnect() + } + } + } + + private fun calcColorsState(project: Project): GitRepositoryColorsState { + val colorManager = RepositoryChangesBrowserNode.getColorManager(project) + + val gitRepositories = GitRepositoryManager.getInstance(project).repositories + val repositoryColors: MutableMap = mutableMapOf() + + for (gitRepository in gitRepositories) { + val color = colorManager.getRootColor(gitRepository.root) + repositoryColors[gitRepository.rpcId] = GitRepositoryColor.of(color) + } + + return GitRepositoryColorsState(repositoryColors) + } + + companion object { + private val LOG = Logger.getInstance(GitRepositoryColorsApiImpl::class.java) + } +} \ No newline at end of file