diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/changes/CodeReviewChangeListComponentFactory.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/changes/CodeReviewChangeListComponentFactory.kt index 146375894ae3..1bf623f81f35 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/changes/CodeReviewChangeListComponentFactory.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/changes/CodeReviewChangeListComponentFactory.kt @@ -1,6 +1,7 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.collaboration.ui.codereview.changes +import com.intellij.collaboration.async.launchNow import com.intellij.collaboration.ui.codereview.CodeReviewProgressTreeModel import com.intellij.collaboration.ui.codereview.details.model.CodeReviewChangeListViewModel import com.intellij.collaboration.ui.codereview.setupCodeReviewProgressModel @@ -11,7 +12,6 @@ import com.intellij.collaboration.util.fileStatus import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataKey import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.FilePath import com.intellij.openapi.vcs.changes.ChangesUtil import com.intellij.openapi.vcs.changes.ui.* @@ -38,7 +38,7 @@ object CodeReviewChangeListComponentFactory { progressModel: CodeReviewProgressTreeModel<*>?, emptyTextText: @Nls String): AsyncChangesTree { val treeModel = createTreeModel(vm) - val tree = createTree(vm.project, treeModel).apply { + val tree = cs.createTree(vm, treeModel).apply { emptyText.text = emptyTextText }.also { tree -> ClientProperty.put(tree, ExpandableItemsHandler.IGNORE_ITEM_SELECTION, true) @@ -89,8 +89,8 @@ object CodeReviewChangeListComponentFactory { return tree } - private fun createTree(project: Project, treeModel: AsyncChangesTreeModel) = - object : AsyncChangesTree(project, false, false) { + private fun CoroutineScope.createTree(vm: CodeReviewChangeListViewModel, treeModel: AsyncChangesTreeModel) = + object : AsyncChangesTree(vm.project, false, false) { override val changesTreeModel: AsyncChangesTreeModel = treeModel override fun getData(dataId: String): Any? { @@ -108,6 +108,23 @@ object CodeReviewChangeListComponentFactory { private fun getSelectedFiles(): List = getSelectedChanges().mapNotNull { it.filePath.virtualFile } + + override fun installGroupingSupport(): ChangesGroupingSupport = + if (vm is CodeReviewChangeListViewModel.WithGrouping) { + ChangesGroupingSupport(vm.project, this, false).also { gs -> + installGroupingSupport(this, gs, vm.grouping::value, vm::setGrouping) + launchNow { + vm.grouping.collect { + if(gs.groupingKeys != it) { + gs.setGroupingKeysOrSkip(it) + } + } + } + } + } + else { + super.installGroupingSupport() + } } } diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/details/model/CodeReviewChangeListViewModel.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/details/model/CodeReviewChangeListViewModel.kt index b705441498ce..a237cf9db842 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/details/model/CodeReviewChangeListViewModel.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/details/model/CodeReviewChangeListViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.jetbrains.annotations.ApiStatus interface CodeReviewChangeListViewModel { val project: Project @@ -50,6 +51,16 @@ interface CodeReviewChangeListViewModel { val detailsByChange: StateFlow> } + @ApiStatus.Experimental + interface WithGrouping : CodeReviewChangeListViewModel { + /** + * A set of enabled grouping policies + */ + val grouping: StateFlow> + + fun setGrouping(grouping: Collection) + } + sealed interface SelectionRequest { data object All : SelectionRequest data class OneChange(val change: RefComparisonChange) : SelectionRequest @@ -63,6 +74,7 @@ interface CodeReviewChangeListViewModel { fun CodeReviewChangeListViewModel.WithDetails.isViewedStateForAllChanges(changes: Iterable, viewed: Boolean): Boolean = changes.all { detailsByChange.value[it]?.isRead == viewed } +@ApiStatus.Internal abstract class CodeReviewChangeListViewModelBase( parentCs: CoroutineScope, protected val changeList: CodeReviewChangeList diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/util/CollectableSerializablePersistentStateComponent.kt b/platform/collaboration-tools/src/com/intellij/collaboration/util/CollectableSerializablePersistentStateComponent.kt index 348f262e0231..01da5ff266be 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/util/CollectableSerializablePersistentStateComponent.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/util/CollectableSerializablePersistentStateComponent.kt @@ -17,8 +17,8 @@ abstract class CollectableSerializablePersistentStateComponent(defaultS } - protected fun updateStateAndEmit(updateFunction: (currentState: T) -> T): T { + protected fun updateStateAndEmit(updateFunction: (currentState: T) -> T) { + updateState(updateFunction) stateFlow.update(updateFunction) - return updateState(updateFunction) } } \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt index fc1b8f7c1818..716c79e05242 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/config/GithubPullRequestsProjectUISettings.kt @@ -2,9 +2,10 @@ package org.jetbrains.plugins.github.pullrequest.config import com.intellij.collaboration.async.mapState +import com.intellij.collaboration.util.CollectableSerializablePersistentStateComponent import com.intellij.openapi.components.* import com.intellij.openapi.project.Project -import kotlinx.coroutines.flow.MutableStateFlow +import com.intellij.openapi.vcs.changes.ui.ChangesGroupingSupport import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable import org.jetbrains.plugins.github.api.GHRepositoryCoordinates @@ -16,10 +17,8 @@ import org.jetbrains.plugins.github.authentication.accounts.GithubAccount @Service(Service.Level.PROJECT) @State(name = "GithubPullRequestsUISettings", storages = [Storage(StoragePathMacros.WORKSPACE_FILE)], reportStatistic = false) -class GithubPullRequestsProjectUISettings(private val project: Project) - : SerializablePersistentStateComponent(SettingsState()) { - - private val stateOfState = MutableStateFlow(SettingsState()) +internal class GithubPullRequestsProjectUISettings(private val project: Project) + : CollectableSerializablePersistentStateComponent(SettingsState()) { @Serializable data class SettingsState( @@ -27,7 +26,8 @@ class GithubPullRequestsProjectUISettings(private val project: Project) val recentNewPullRequestHead: RepoCoordinatesHolder? = null, val reviewCommentPreferred: Boolean = true, val editorReviewEnabled: Boolean = true, - val highlightDiffLinesInEditor: Boolean = false + val highlightDiffLinesInEditor: Boolean = false, + val changesGrouping: Set = setOf(ChangesGroupingSupport.DIRECTORY_GROUPING, ChangesGroupingSupport.MODULE_GROUPING) ) var selectedUrlAndAccount: Pair? @@ -36,7 +36,7 @@ class GithubPullRequestsProjectUISettings(private val project: Project) val account = GHAccountSerializer.deserialize(accountId) ?: return null return url to account } - set(value) = updateAndSetState { + set(value) = updateStateAndEmit { it.copy(selectedUrlAndAccountId = value?.let { (repo, account) -> UrlAndAccount(repo, GHAccountSerializer.serialize(account)) }) @@ -44,40 +44,40 @@ class GithubPullRequestsProjectUISettings(private val project: Project) var recentNewPullRequestHead: GHRepositoryCoordinates? get() = state.recentNewPullRequestHead?.let { GHRepositoryCoordinates(it.server, GHRepositoryPath(it.owner, it.repository)) } - set(value) = updateAndSetState { + set(value) = updateStateAndEmit { it.copy(recentNewPullRequestHead = value?.let { RepoCoordinatesHolder(it) }) } var reviewCommentsPreferred: Boolean get() = state.reviewCommentPreferred - set(value) = updateAndSetState { + set(value) = updateStateAndEmit { it.copy(reviewCommentPreferred = value) } var editorReviewEnabled: Boolean get() = state.editorReviewEnabled - set(value) = updateAndSetState { + set(value) = updateStateAndEmit { it.copy(editorReviewEnabled = value) } var highlightDiffLinesInEditor: Boolean get() = state.highlightDiffLinesInEditor - set(value) = updateAndSetState { + set(value) = updateStateAndEmit { it.copy(highlightDiffLinesInEditor = value) } - val reviewCommentsPreferredState: StateFlow = stateOfState.mapState { it.reviewCommentPreferred } - val editorReviewEnabledState: StateFlow = stateOfState.mapState { it.editorReviewEnabled } - val highlightDiffLinesInEditorState: StateFlow = stateOfState.mapState { it.highlightDiffLinesInEditor } + var changesGrouping: Set + get() = state.changesGrouping + set(value) { + updateStateAndEmit { + it.copy(changesGrouping = value) + } + } + val changesGroupingState: StateFlow> = stateFlow.mapState { it.changesGrouping } - override fun loadState(state: SettingsState) { - super.loadState(state) - stateOfState.value = state - } - - private inline fun updateAndSetState(updateFunction: (currentState: SettingsState) -> SettingsState) { - stateOfState.value = updateState(updateFunction) - } + val reviewCommentsPreferredState: StateFlow = stateFlow.mapState { it.reviewCommentPreferred } + val editorReviewEnabledState: StateFlow = stateFlow.mapState { it.editorReviewEnabled } + val highlightDiffLinesInEditorState: StateFlow = stateFlow.mapState { it.highlightDiffLinesInEditor } companion object { @JvmStatic diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/details/model/GHPRChangeListViewModel.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/details/model/GHPRChangeListViewModel.kt index 0d53dd9c9fc4..7990fd62036d 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/details/model/GHPRChangeListViewModel.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/details/model/GHPRChangeListViewModel.kt @@ -14,13 +14,14 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.github.api.data.pullrequest.isViewed +import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProjectUISettings import org.jetbrains.plugins.github.pullrequest.data.GHPRDataContext import org.jetbrains.plugins.github.pullrequest.data.provider.GHPRDataProvider import org.jetbrains.plugins.github.pullrequest.data.provider.createThreadsRequestsFlow import org.jetbrains.plugins.github.pullrequest.data.provider.createViewedStateRequestsFlow @ApiStatus.Experimental -interface GHPRChangeListViewModel : CodeReviewChangeListViewModel.WithDetails { +interface GHPRChangeListViewModel : CodeReviewChangeListViewModel.WithDetails, CodeReviewChangeListViewModel.WithGrouping { val isUpdating: StateFlow val isOnLatest: Boolean @@ -40,6 +41,7 @@ internal class GHPRChangeListViewModelImpl( changes: CodeReviewChangesContainer, changeList: CodeReviewChangeList ) : GHPRChangeListViewModel, CodeReviewChangeListViewModelBase(parentCs, changeList) { + private val preferences = GithubPullRequestsProjectUISettings.getInstance(project) private val repository: GitRepository get() = dataContext.repositoryDataService.remoteCoordinates.repository private val _isUpdating = MutableStateFlow(false) @@ -57,6 +59,8 @@ internal class GHPRChangeListViewModelImpl( MutableStateFlow(emptyMap()) } + override val grouping: StateFlow> = preferences.changesGroupingState + fun setUpdating(updating: Boolean) { _isUpdating.value = updating } @@ -85,6 +89,10 @@ internal class GHPRChangeListViewModelImpl( } } + override fun setGrouping(grouping: Collection) { + preferences.changesGrouping = grouping.toSet() + } + private fun createDetailsByChangeFlow(): Flow> { val threadsFlow = dataProvider.reviewData.createThreadsRequestsFlow().values() val viewedStateFlow = viewedStateData.createViewedStateRequestsFlow().values() diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/GitLabMergeRequestsPreferences.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/GitLabMergeRequestsPreferences.kt index 90674d4b98d7..e74aba48996e 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/GitLabMergeRequestsPreferences.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/GitLabMergeRequestsPreferences.kt @@ -5,6 +5,7 @@ import com.intellij.collaboration.async.mapState import com.intellij.collaboration.util.CollectableSerializablePersistentStateComponent import com.intellij.openapi.components.* import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.changes.ui.ChangesGroupingSupport import git4idea.remote.hosting.knownRepositories import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable @@ -24,7 +25,8 @@ internal class GitLabMergeRequestsPreferences(private val project: Project) val showEventsInTimeline: Boolean = true, val highlightDiffLinesInEditor: Boolean = true, val usedAsDraftSubmitActionLast: Boolean = true, - val editorReviewEnabled: Boolean = true + val editorReviewEnabled: Boolean = true, + val changesGrouping: Set = setOf(ChangesGroupingSupport.DIRECTORY_GROUPING, ChangesGroupingSupport.MODULE_GROUPING) ) var selectedRepoAndAccount: Pair? @@ -78,4 +80,13 @@ internal class GitLabMergeRequestsPreferences(private val project: Project) it.copy(editorReviewEnabled = value) } } + + var changesGrouping: Set + get() = state.changesGrouping + set(value) { + updateStateAndEmit { + it.copy(changesGrouping = value) + } + } + val changesGroupingState: StateFlow> = stateFlow.mapState { it.changesGrouping } } \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangeListViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangeListViewModel.kt index df5f7e1b2ef8..1d1c9baf66d1 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangeListViewModel.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangeListViewModel.kt @@ -15,10 +15,13 @@ import git4idea.changes.GitBranchComparisonResult import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.jetbrains.plugins.gitlab.api.GitLabId +import org.jetbrains.plugins.gitlab.mergerequest.GitLabMergeRequestsPreferences import org.jetbrains.plugins.gitlab.mergerequest.data.* import java.util.concurrent.ConcurrentHashMap -interface GitLabMergeRequestChangeListViewModel : CodeReviewChangeListViewModel.WithDetails { +interface GitLabMergeRequestChangeListViewModel + : CodeReviewChangeListViewModel.WithDetails, + CodeReviewChangeListViewModel.WithGrouping { val isOnLatest: Boolean fun setViewedState(changes: Iterable, viewed: Boolean) @@ -33,6 +36,7 @@ internal class GitLabMergeRequestChangeListViewModelImpl( ) : CodeReviewChangeListViewModelBase(parentCs, changeList), GitLabMergeRequestChangeListViewModel { private val persistentChangesViewedState = project.service() + private val preferences = project.service() private val _showDiffRequests = MutableSharedFlow() val showDiffRequests: Flow = _showDiffRequests.asSharedFlow() @@ -67,6 +71,8 @@ internal class GitLabMergeRequestChangeListViewModelImpl( } }.stateIn(cs, SharingStarted.Eagerly, emptyMap()) + override val grouping: StateFlow> = preferences.changesGroupingState + override fun showDiffPreview() { cs.launch { _showDiffRequests.emit(Unit) @@ -90,6 +96,10 @@ internal class GitLabMergeRequestChangeListViewModelImpl( viewed ) } + + override fun setGrouping(grouping: Collection) { + preferences.changesGrouping = grouping.toSet() + } } @OptIn(ExperimentalCoroutinesApi::class)