IJPL-191999: vcs: Add asynchronous repository color syncing

- Introduced `GitRepositoryColorsApi` for syncing repository colors.
- Implemented `GitRepositoryColorsApiImpl` and supporting classes.
- Updated repository color retrieval using `GitRepositoryColorsHolder`.
- Adjusted usage in affected UI components and constructors.


IJ-MR-166259

GitOrigin-RevId: da87ca59c1cb5b2e76d52d7c5ea3d528eb40a79a
This commit is contained in:
Aleksandr Krasilnikov
2025-07-10 15:59:33 +00:00
committed by intellij-monorepo-bot
parent 778814aef5
commit d5159c2e51
8 changed files with 189 additions and 13 deletions
@@ -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)
}
}
@@ -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
}
}
@@ -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<RepositoryId, GitRepositoryColor> = 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()
}
}
@@ -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<Unit> {
suspend fun syncColors(projectId: ProjectId) : Flow<GitRepositoryColorsState>
companion object {
suspend fun getInstance() : GitRepositoryColorsApi =
RemoteApiProviderService.resolve(remoteApiDescriptor<GitRepositoryColorsApi>())
}
}
@@ -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: 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)
}
@@ -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<GitRepository?> {
val items = listOf(ALL_REPOSITORIES, *allRepositories.toTypedArray())
val colorGenerator = RepositoryColorGeneratorFactory.create(allRepositories.map { it.rpcId })
return ComboBox(CollectionComboBoxModel(items)).apply {
renderer = listCellRenderer<GitRepository?>(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))
}
}
@@ -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<GitIncomingOutgoingStateApi>()) {
GitIncomingOutgoingStateApiImpl()
}
remoteApi(remoteApiDescriptor<GitRepositoryColorsApi>()) {
GitRepositoryColorsApiImpl()
}
remoteApi(remoteApiDescriptor<GitUiSettingsApi>()) {
GitUiSettingsApiImpl()
}
@@ -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<GitRepositoryColorsState> {
requireOwner()
val project = projectId.findProjectOrNull() ?: return emptyFlow()
return callbackFlow {
val notifier = MutableSharedFlow<Unit>(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<RepositoryId, GitRepositoryColor> = 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)
}
}