From 75ed06f7465bb09bee09659e472235ff927d2659 Mon Sep 17 00:00:00 2001 From: Julia Beliaeva Date: Wed, 22 May 2024 21:33:05 +0200 Subject: [PATCH] [lvcs] support different modes when computing differences between directories When a single revision in the directory history is selected, differences are computed with the local directory version. This will return all the changes made in the revision, plus all the changes made afterward. This commit adds support for comparison with the next revision, to see only changes made in the revision. IJPL-85116 GitOrigin-RevId: 8c09bfdda925c859cbb226467ebfe4b63854eaf1 --- platform/lvcs-impl/api-dump-unreviewed.txt | 10 +++++- .../platform/lvcs/impl/ActivityProvider.kt | 7 +++- .../lvcs/impl/LocalHistoryActivityProvider.kt | 4 +-- .../impl/actions/ChangeSetSelectionAction.kt | 4 +-- .../lvcs/impl/actions/CreatePatchAction.kt | 9 ++++-- .../lvcs/impl/actions/RevertAction.kt | 8 +++-- .../lvcs/impl/diff/ActivityDiffDataImpl.kt | 3 +- .../platform/lvcs/impl/diff/DiffUtils.kt | 32 ++++++++++++++++--- .../platform/lvcs/impl/operations/Revert.kt | 4 +-- .../platform/lvcs/impl/ui/ActivityView.kt | 1 + .../lvcs/impl/ui/ActivityViewDataKeys.kt | 2 ++ .../lvcs/impl/ui/ActivityViewModel.kt | 8 +++-- .../impl/LocalHistoryActivityProviderTest.kt | 32 +++++++++++++++++-- 13 files changed, 101 insertions(+), 23 deletions(-) diff --git a/platform/lvcs-impl/api-dump-unreviewed.txt b/platform/lvcs-impl/api-dump-unreviewed.txt index f491e47facaa..878b82533727 100644 --- a/platform/lvcs-impl/api-dump-unreviewed.txt +++ b/platform/lvcs-impl/api-dump-unreviewed.txt @@ -827,7 +827,7 @@ f:com.intellij.openapi.command.impl.FileUndoProvider - a:getPresentation(com.intellij.platform.lvcs.impl.ActivityItem):com.intellij.platform.lvcs.impl.ActivityPresentation - a:getSupportedFilterKindFor(com.intellij.platform.lvcs.impl.ActivityScope):com.intellij.platform.lvcs.impl.FilterKind - a:loadActivityList(com.intellij.platform.lvcs.impl.ActivityScope,java.lang.String):com.intellij.platform.lvcs.impl.ActivityData -- a:loadDiffData(com.intellij.platform.lvcs.impl.ActivityScope,com.intellij.platform.lvcs.impl.ActivitySelection):com.intellij.platform.lvcs.impl.ActivityDiffData +- a:loadDiffData(com.intellij.platform.lvcs.impl.ActivityScope,com.intellij.platform.lvcs.impl.ActivitySelection,com.intellij.platform.lvcs.impl.DirectoryDiffMode):com.intellij.platform.lvcs.impl.ActivityDiffData - a:loadSingleDiff(com.intellij.platform.lvcs.impl.ActivityScope,com.intellij.platform.lvcs.impl.ActivitySelection):com.intellij.diff.chains.DiffRequestProducer *:com.intellij.platform.lvcs.impl.ActivityScope - *sf:Companion:com.intellij.platform.lvcs.impl.ActivityScope$Companion @@ -953,6 +953,13 @@ f:com.intellij.platform.lvcs.impl.ChangeSetSelectionKt - sf:getLeftRevision(com.intellij.platform.lvcs.impl.ChangeSetSelection):com.intellij.platform.lvcs.impl.RevisionId - sf:getRightRevision(com.intellij.platform.lvcs.impl.ChangeSetSelection):com.intellij.platform.lvcs.impl.RevisionId - sf:toChangeSetSelection(com.intellij.platform.lvcs.impl.ActivitySelection):com.intellij.platform.lvcs.impl.ChangeSetSelection +*e:com.intellij.platform.lvcs.impl.DirectoryDiffMode +- java.lang.Enum +- sf:WithLocal:com.intellij.platform.lvcs.impl.DirectoryDiffMode +- sf:WithNext:com.intellij.platform.lvcs.impl.DirectoryDiffMode +- s:getEntries():kotlin.enums.EnumEntries +- s:valueOf(java.lang.String):com.intellij.platform.lvcs.impl.DirectoryDiffMode +- s:values():com.intellij.platform.lvcs.impl.DirectoryDiffMode[] *e:com.intellij.platform.lvcs.impl.FilterKind - java.lang.Enum - sf:CONTENT:com.intellij.platform.lvcs.impl.FilterKind @@ -1066,6 +1073,7 @@ f:com.intellij.platform.lvcs.impl.ui.ActivityView$Companion - f:showInToolWindow(com.intellij.openapi.project.Project,com.intellij.history.integration.IdeaGateway,com.intellij.platform.lvcs.impl.ActivityScope):V *f:com.intellij.platform.lvcs.impl.ui.ActivityViewDataKeys - sf:INSTANCE:com.intellij.platform.lvcs.impl.ui.ActivityViewDataKeys +- f:getDIRECTORY_DIFF_MODE():com.intellij.openapi.actionSystem.DataKey - f:getSCOPE():com.intellij.openapi.actionSystem.DataKey - f:getSELECTED_DIFFERENCES():com.intellij.openapi.actionSystem.DataKey - f:getSELECTION():com.intellij.openapi.actionSystem.DataKey diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ActivityProvider.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ActivityProvider.kt index e57313310cc7..3f201c1b5e7b 100644 --- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ActivityProvider.kt +++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ActivityProvider.kt @@ -12,7 +12,7 @@ interface ActivityProvider { fun loadActivityList(scope: ActivityScope, fileFilter: String?): ActivityData fun filterActivityList(scope: ActivityScope, data: ActivityData, contentFilter: String?): Set? - fun loadDiffData(scope: ActivityScope, selection: ActivitySelection): ActivityDiffData? + fun loadDiffData(scope: ActivityScope, selection: ActivitySelection, diffMode: DirectoryDiffMode): ActivityDiffData? fun loadSingleDiff(scope: ActivityScope, selection: ActivitySelection): DiffRequestProducer? fun getSupportedFilterKindFor(scope: ActivityScope): FilterKind @@ -23,4 +23,9 @@ interface ActivityProvider { @ApiStatus.Experimental enum class FilterKind { FILE, CONTENT +} + +@ApiStatus.Experimental +enum class DirectoryDiffMode { + WithLocal, WithNext } \ No newline at end of file diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/LocalHistoryActivityProvider.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/LocalHistoryActivityProvider.kt index 8139806fe8e7..4f4c415a7fdd 100644 --- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/LocalHistoryActivityProvider.kt +++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/LocalHistoryActivityProvider.kt @@ -131,9 +131,9 @@ internal class LocalHistoryActivityProvider(val project: Project, private val ga return data.items.filterTo(mutableSetOf()) { (it is ChangeSetActivityItem) && filteredIds.contains(it.id) } } - override fun loadDiffData(scope: ActivityScope, selection: ActivitySelection): ActivityDiffData? { + override fun loadDiffData(scope: ActivityScope, selection: ActivitySelection, diffMode: DirectoryDiffMode): ActivityDiffData? { val changeSetSelection = selection.toChangeSetSelection() ?: return null - return facade.createDiffData(gateway, scope, changeSetSelection, USE_OLD_CONTENT) + return facade.createDiffData(gateway, scope, changeSetSelection, diffMode, USE_OLD_CONTENT) } override fun loadSingleDiff(scope: ActivityScope, selection: ActivitySelection): DiffRequestProducer? { diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/ChangeSetSelectionAction.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/ChangeSetSelectionAction.kt index 6833d114d0b0..22cd21452a56 100644 --- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/ChangeSetSelectionAction.kt +++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/ChangeSetSelectionAction.kt @@ -45,9 +45,9 @@ abstract class ChangeSetSelectionAction : DumbAwareAction() { val selection = activitySelection.toChangeSetSelection() ?: return - actionPerformed(project, facade, gateway, activityScope, selection) + actionPerformed(project, facade, gateway, activityScope, selection, e) } abstract fun actionPerformed(project: Project, facade: LocalHistoryFacade, gateway: IdeaGateway, - activityScope: ActivityScope, selection: ChangeSetSelection) + activityScope: ActivityScope, selection: ChangeSetSelection, e: AnActionEvent) } \ No newline at end of file diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/CreatePatchAction.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/CreatePatchAction.kt index 42baf3719c70..007647008c16 100644 --- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/CreatePatchAction.kt +++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/CreatePatchAction.kt @@ -7,6 +7,7 @@ import com.intellij.history.core.revisions.Difference import com.intellij.history.integration.IdeaGateway import com.intellij.history.integration.LocalHistoryBundle import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages @@ -15,9 +16,11 @@ import com.intellij.openapi.vcs.changes.Change import com.intellij.openapi.vcs.changes.actions.CreatePatchFromChangesAction import com.intellij.platform.lvcs.impl.ActivityScope import com.intellij.platform.lvcs.impl.ChangeSetSelection +import com.intellij.platform.lvcs.impl.DirectoryDiffMode import com.intellij.platform.lvcs.impl.USE_OLD_CONTENT import com.intellij.platform.lvcs.impl.diff.getDiff import com.intellij.platform.lvcs.impl.statistics.LocalHistoryCounter +import com.intellij.platform.lvcs.impl.ui.ActivityViewDataKeys internal class CreatePatchAction : ChangeSetSelectionAction() { @@ -25,11 +28,13 @@ internal class CreatePatchAction : ChangeSetSelectionAction() { facade: LocalHistoryFacade, gateway: IdeaGateway, activityScope: ActivityScope, - selection: ChangeSetSelection) { + selection: ChangeSetSelection, + e: AnActionEvent) { LocalHistoryCounter.logActionInvoked(LocalHistoryCounter.ActionKind.CreatePatch, activityScope) + val diffMode = e.getData(ActivityViewDataKeys.DIRECTORY_DIFF_MODE) ?: DirectoryDiffMode.WithLocal val changes = ProgressManager.getInstance().runProcessWithProgressSynchronously(ThrowableComputable { - val diff = facade.getDiff(gateway, activityScope, selection, USE_OLD_CONTENT) + val diff = facade.getDiff(gateway, activityScope, selection, diffMode, USE_OLD_CONTENT) if (diff.any { it.left?.hasUnavailableContent() == true || it.right?.hasUnavailableContent() == true }) { return@ThrowableComputable null } diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/RevertAction.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/RevertAction.kt index 281621e28418..34d61d9e0c12 100644 --- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/RevertAction.kt +++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/actions/RevertAction.kt @@ -4,9 +4,11 @@ package com.intellij.platform.lvcs.impl.actions import com.intellij.history.core.LocalHistoryFacade import com.intellij.history.integration.IdeaGateway import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project import com.intellij.platform.lvcs.impl.ActivityScope import com.intellij.platform.lvcs.impl.ChangeSetSelection +import com.intellij.platform.lvcs.impl.DirectoryDiffMode import com.intellij.platform.lvcs.impl.USE_OLD_CONTENT import com.intellij.platform.lvcs.impl.operations.createReverter import com.intellij.platform.lvcs.impl.statistics.LocalHistoryCounter @@ -21,10 +23,12 @@ internal class RevertAction : ChangeSetSelectionAction() { facade: LocalHistoryFacade, gateway: IdeaGateway, activityScope: ActivityScope, - selection: ChangeSetSelection) { + selection: ChangeSetSelection, + e: AnActionEvent) { LocalHistoryCounter.logActionInvoked(LocalHistoryCounter.ActionKind.RevertRevisions, activityScope) - val reverter = facade.createReverter(project, gateway, activityScope, selection, USE_OLD_CONTENT) + val diffMode = DirectoryDiffMode.WithLocal // revert everything after the selected changeset + val reverter = facade.createReverter(project, gateway, activityScope, selection, diffMode, USE_OLD_CONTENT) if (reverter == null || reverter.checkCanRevert().isNotEmpty()) return reverter.revert() } diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/diff/ActivityDiffDataImpl.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/diff/ActivityDiffDataImpl.kt index b93038349487..6f8e56dfcf95 100644 --- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/diff/ActivityDiffDataImpl.kt +++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/diff/ActivityDiffDataImpl.kt @@ -15,8 +15,9 @@ private class ActivityDiffDataImpl(override val presentableChanges: Iterable
,
+                                        diffMode: DirectoryDiffMode,
                                         isOldContentUsed: Boolean): List {
-  return entryPaths.flatMap {
-    val leftEntry = findEntry(rootEntry, selection.leftRevision, it, isOldContentUsed)
-    val rightEntry = findEntry(rootEntry, selection.rightRevision, it, isOldContentUsed)
-    Entry.getDifferencesBetween(leftEntry, rightEntry, selection.rightRevision is RevisionId.Current)
+  val leftRevision = selection.leftRevision
+  val rightRevision = when (diffMode) {
+    DirectoryDiffMode.WithLocal -> selection.rightRevision
+    DirectoryDiffMode.WithNext -> {
+      val rightItem = selection.rightItem
+      if (rightItem != null) RevisionId.ChangeSet(rightItem.id) else nextRevision(leftRevision)
+    }
   }
+  return entryPaths.flatMap {
+    val leftEntry = findEntry(rootEntry, leftRevision, it, isOldContentUsed)
+    val rightEntry = findEntry(rootEntry, rightRevision, it, isOldContentUsed)
+    Entry.getDifferencesBetween(leftEntry, rightEntry, rightRevision is RevisionId.Current)
+  }
+}
+
+private fun LocalHistoryFacade.nextRevision(revisionId: RevisionId): RevisionId {
+  if (revisionId is RevisionId.Current) return RevisionId.Current
+  revisionId as RevisionId.ChangeSet
+
+  var nextChange: Long? = null
+  for (change in changes) {
+    if (change.id == revisionId.id) break
+    nextChange = change.id
+  }
+  return nextChange?.let { RevisionId.ChangeSet(it) } ?: RevisionId.Current
 }
 
 internal fun LocalHistoryFacade.getDiff(gateway: IdeaGateway,
                                         scope: ActivityScope,
                                         selection: ChangeSetSelection,
+                                        diffMode: DirectoryDiffMode,
                                         isOldContentUsed: Boolean): List {
   val rootEntry = selection.data.getRootEntry(gateway)
   val entryPaths = getEntryPaths(gateway, scope)
-  return getDiff(rootEntry, selection, entryPaths, isOldContentUsed).toList()
+  return getDiff(rootEntry, selection, entryPaths, diffMode, isOldContentUsed).toList()
 }
 
 internal fun getEntryPaths(gateway: IdeaGateway, scope: ActivityScope): Collection {
diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/operations/Revert.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/operations/Revert.kt
index bccc832b4d12..24028dd9415d 100644
--- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/operations/Revert.kt
+++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/operations/Revert.kt
@@ -18,7 +18,7 @@ import org.jetbrains.annotations.Nls
 import java.util.function.Supplier
 
 internal fun LocalHistoryFacade.createReverter(project: Project, gateway: IdeaGateway, scope: ActivityScope, selection: ChangeSetSelection,
-                                               isOldContentUsed: Boolean): Reverter? {
+                                               diffMode: DirectoryDiffMode, isOldContentUsed: Boolean): Reverter? {
   val targetRevisionId = selection.leftRevision
   if (targetRevisionId == RevisionId.Current) return null
 
@@ -32,7 +32,7 @@ internal fun LocalHistoryFacade.createReverter(project: Project, gateway: IdeaGa
                              commandNameSupplier)
   }
 
-  val diff = getDiff(gateway, scope, selection, isOldContentUsed)
+  val diff = getDiff(gateway, scope, selection, diffMode, isOldContentUsed)
   return DifferenceReverter(project, this, gateway, diff, commandNameSupplier)
 }
 
diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityView.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityView.kt
index d64bacb99793..b49f1114bb0c 100644
--- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityView.kt
+++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityView.kt
@@ -151,6 +151,7 @@ class ActivityView(private val project: Project, gateway: IdeaGateway, val activ
     if (ActivityViewDataKeys.SELECTION.`is`(dataId)) return activityList.selection
     if (ActivityViewDataKeys.SCOPE.`is`(dataId)) return activityScope
     if (EditorTabDiffPreviewManager.EDITOR_TAB_DIFF_PREVIEW.`is`(dataId)) return editorDiffPreview
+    if (ActivityViewDataKeys.DIRECTORY_DIFF_MODE.`is`(dataId)) return model.diffMode
     return null
   }
 
diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewDataKeys.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewDataKeys.kt
index 0c998cfeda5b..b2f5b3589e23 100644
--- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewDataKeys.kt
+++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewDataKeys.kt
@@ -5,6 +5,7 @@ import com.intellij.openapi.actionSystem.DataKey
 import com.intellij.openapi.vcs.changes.ui.PresentableChange
 import com.intellij.platform.lvcs.impl.ActivityScope
 import com.intellij.platform.lvcs.impl.ActivitySelection
+import com.intellij.platform.lvcs.impl.DirectoryDiffMode
 import org.jetbrains.annotations.ApiStatus
 
 @ApiStatus.Experimental
@@ -12,4 +13,5 @@ object ActivityViewDataKeys {
   val SELECTION: DataKey = DataKey.create("ActivityView.Selection")
   val SCOPE: DataKey = DataKey.create("ActivityView.Scope")
   val SELECTED_DIFFERENCES: DataKey> = DataKey.create("ActivityView.SelectedDifferences")
+  val DIRECTORY_DIFF_MODE: DataKey = DataKey.create("ActivityView.DirectoryDiffMode")
 }
\ No newline at end of file
diff --git a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewModel.kt b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewModel.kt
index b412e9fd7c42..21e5d24dcc18 100644
--- a/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewModel.kt
+++ b/platform/lvcs-impl/src/com/intellij/platform/lvcs/impl/ui/ActivityViewModel.kt
@@ -22,6 +22,7 @@ internal class ActivityViewModel(private val project: Project, gateway: IdeaGate
 
   private val activityItemsFlow = MutableStateFlow(ActivityData.EMPTY)
   private val selectionFlow = MutableStateFlow(null)
+  private val diffModeFlow = MutableStateFlow(DirectoryDiffMode.WithLocal)
 
   private val filterFlow = MutableStateFlow(null)
 
@@ -50,15 +51,15 @@ internal class ActivityViewModel(private val project: Project, gateway: IdeaGate
     }
     if (!isSingleDiffSupported) {
       coroutineScope.launch {
-        selectionFlow.collectLatest { selection ->
-          thisLogger().debug("Loading diff data for $activityScope")
+        combine(selectionFlow, diffModeFlow) { s, d -> s to d }.collectLatest { (selection, diffMode) ->
+          thisLogger().debug("Loading diff data for $activityScope diff mode $diffMode")
           withContext(Dispatchers.EDT) {
             eventDispatcher.multicaster.onDiffDataLoadingStarted()
           }
           val diffData = selection?.let {
             withContext(Dispatchers.Default) {
               LocalHistoryCounter.logLoadDiff(project, activityScope) {
-                activityProvider.loadDiffData(activityScope, selection)
+                activityProvider.loadDiffData(activityScope, selection, diffMode)
               }
             }
           }
@@ -89,6 +90,7 @@ internal class ActivityViewModel(private val project: Project, gateway: IdeaGate
   }
 
   internal val selection get() = selectionFlow.value
+  internal val diffMode get() = diffModeFlow.value
 
   internal val isSingleDiffSupported get() = !activityScope.hasMultipleFiles
 
diff --git a/platform/lvcs-impl/testSrc/com/intellij/platform/lvcs/impl/LocalHistoryActivityProviderTest.kt b/platform/lvcs-impl/testSrc/com/intellij/platform/lvcs/impl/LocalHistoryActivityProviderTest.kt
index 9d7a4727b2e5..2c428a3337c5 100644
--- a/platform/lvcs-impl/testSrc/com/intellij/platform/lvcs/impl/LocalHistoryActivityProviderTest.kt
+++ b/platform/lvcs-impl/testSrc/com/intellij/platform/lvcs/impl/LocalHistoryActivityProviderTest.kt
@@ -234,12 +234,40 @@ class LocalHistoryActivityProviderTest : IntegrationTestCase() {
     TestCase.assertEquals(listOf("ADDED:${file.name}"), getDiffDataForSelection(provider, scope, activityList, 3, 4))
     TestCase.assertEquals(listOf("ADDED:${file.name}", "ADDED:${directory.name}", "ADDED:${innerDirectory.name}").sorted(),
                           getDiffDataForSelection(provider, scope, activityList, 0, 4))
+
+    TestCase.assertEquals(listOf("MODIFIED:${file.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithNext, 0))
+    TestCase.assertEquals(listOf("MODIFIED:${file.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithLocal, 0))
+
+    TestCase.assertEquals(listOf("MODIFIED:${file.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithNext, 1))
+    TestCase.assertEquals(listOf("MODIFIED:${file.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithLocal, 1))
+
+    TestCase.assertEquals(listOf("ADDED:${innerDirectory.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithNext, 2))
+    TestCase.assertEquals(listOf("MODIFIED:${file.name}", "ADDED:${innerDirectory.name}").sorted(),
+                          getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithLocal, 2))
+
+    TestCase.assertEquals(listOf("ADDED:${directory.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithNext, 3))
+    TestCase.assertEquals(listOf("MODIFIED:${file.name}", "ADDED:${directory.name}", "ADDED:${innerDirectory.name}").sorted(),
+                          getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithLocal, 3))
+
+    TestCase.assertEquals(listOf("ADDED:${file.name}"), getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithNext, 4))
+    TestCase.assertEquals(listOf("ADDED:${file.name}", "ADDED:${directory.name}", "ADDED:${innerDirectory.name}").sorted(),
+                          getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithLocal, 4))
+  }
+
+  private fun getDiffDataForSelection(provider: LocalHistoryActivityProvider, scope: ActivityScope, activityList: ActivityData,
+                                      diffMode: DirectoryDiffMode, index: Int): List {
+    return getDiffDataForSelection(provider, scope, activityList, diffMode, index, index)
   }
 
   private fun getDiffDataForSelection(provider: LocalHistoryActivityProvider, scope: ActivityScope, activityList: ActivityData,
                                       from: Int, to: Int): List {
-    val selection = ActivitySelection(listOf(activityList.items[from], activityList.items[to]), activityList)
-    return provider.loadDiffData(scope, selection)!!.presentableChanges.map { "${it.fileStatus}:${it.filePath.name}" }.sorted()
+    return getDiffDataForSelection(provider, scope, activityList, DirectoryDiffMode.WithLocal, from, to)
+  }
+
+  private fun getDiffDataForSelection(provider: LocalHistoryActivityProvider, scope: ActivityScope, activityList: ActivityData,
+                                      diffMode: DirectoryDiffMode, from: Int, to: Int): List {
+    val selection = ActivitySelection(listOf(from, to).distinct().map { activityList.items[it] }, activityList)
+    return provider.loadDiffData(scope, selection, diffMode)!!.presentableChanges.map { "${it.fileStatus}:${it.filePath.name}" }.sorted()
   }
 
   private fun ActivityData.getLabelNameSet(): Set {