diff --git a/java/java-tests/BUILD.bazel b/java/java-tests/BUILD.bazel
index ef54ebfd7130..895adeebf0a0 100644
--- a/java/java-tests/BUILD.bazel
+++ b/java/java-tests/BUILD.bazel
@@ -46,6 +46,7 @@ jvm_library(
"//platform/util/diff",
"//platform/vcs-api:vcs",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/editor-ui-ex:editor-ex",
"//java/execution/impl",
"//platform/diff-api:diff",
diff --git a/platform/collaboration-tools/BUILD.bazel b/platform/collaboration-tools/BUILD.bazel
index 49746043b050..46b74346f924 100644
--- a/platform/collaboration-tools/BUILD.bazel
+++ b/platform/collaboration-tools/BUILD.bazel
@@ -112,6 +112,7 @@ jvm_library(
"@lib//:app-cash-turbine-jvm",
"@lib//:assert_j",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
]
)
### auto-generated section `build intellij.platform.collaborationTools` end
diff --git a/platform/dvcs-impl/BUILD.bazel b/platform/dvcs-impl/BUILD.bazel
index d5ce8721fe2a..d14bcac129da 100644
--- a/platform/dvcs-impl/BUILD.bazel
+++ b/platform/dvcs-impl/BUILD.bazel
@@ -65,6 +65,7 @@ jvm_library(
"//platform/vcs-impl:vcs-impl_test_lib",
"//platform/project/shared:project",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/kernel/shared:kernel",
"//platform/core-impl",
"//platform/platform-impl:ide-impl",
diff --git a/platform/lvcs-impl/BUILD.bazel b/platform/lvcs-impl/BUILD.bazel
index d00fc1780be5..bc9fc16d7c0b 100644
--- a/platform/lvcs-impl/BUILD.bazel
+++ b/platform/lvcs-impl/BUILD.bazel
@@ -82,6 +82,7 @@ jvm_library(
"//tools/intellij.tools.ide.metrics.benchmark:ide-metrics-benchmark",
"//tools/intellij.tools.ide.metrics.benchmark:ide-metrics-benchmark_test_lib",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"@lib//:jetbrains-annotations",
"//platform/projectModel-impl",
]
diff --git a/platform/platform-tests/BUILD.bazel b/platform/platform-tests/BUILD.bazel
index da9c27948656..42e8d6b8ccc5 100644
--- a/platform/platform-tests/BUILD.bazel
+++ b/platform/platform-tests/BUILD.bazel
@@ -176,6 +176,7 @@ jvm_library(
"//platform/locking.impl",
"//platform/testFramework/junit5/highlighting",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
],
runtime_deps = [
"//xml/dom-impl",
diff --git a/platform/vcs-api/shared/src/com/intellij/platform/vcs/changes/ChangesUtil.kt b/platform/vcs-api/shared/src/com/intellij/platform/vcs/changes/ChangesUtil.kt
index cc3e8a3a4052..1a08f8e9f7d7 100644
--- a/platform/vcs-api/shared/src/com/intellij/platform/vcs/changes/ChangesUtil.kt
+++ b/platform/vcs-api/shared/src/com/intellij/platform/vcs/changes/ChangesUtil.kt
@@ -82,4 +82,7 @@ object ChangesUtil {
status === FileStatus.MERGED_WITH_BOTH_CONFLICTS ||
status === FileStatus.MERGED_WITH_PROPERTY_CONFLICTS
}
+
+ @JvmStatic
+ fun matches(change: Change, path: FilePath): Boolean = path == getAfterPath(change) || path == getBeforePath(change)
}
\ No newline at end of file
diff --git a/platform/vcs-impl/BUILD.bazel b/platform/vcs-impl/BUILD.bazel
index 21d311a08365..fe520fd497ea 100644
--- a/platform/vcs-impl/BUILD.bazel
+++ b/platform/vcs-impl/BUILD.bazel
@@ -155,6 +155,7 @@ jvm_library(
"//libraries/microba",
"//platform/kernel/shared:kernel",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"@lib//:jgoodies-forms",
"//platform/observable",
"//platform/platform-impl/concurrency",
diff --git a/platform/vcs-impl/frontend/BUILD.bazel b/platform/vcs-impl/frontend/BUILD.bazel
index 1e2c4de11b93..2f389aa0aa49 100644
--- a/platform/vcs-impl/frontend/BUILD.bazel
+++ b/platform/vcs-impl/frontend/BUILD.bazel
@@ -38,4 +38,48 @@ jvm_library(
"//platform/vcs-api/vcs-api-core:vcs-core",
]
)
-### auto-generated section `build intellij.platform.vcs.impl.frontend` end
\ No newline at end of file
+
+jvm_library(
+ name = "frontend_test_lib",
+ visibility = ["//visibility:public"],
+ srcs = glob(["testSrc/**/*.kt", "testSrc/**/*.java", "testSrc/**/*.form"], allow_empty = True),
+ associates = [":frontend"],
+ deps = [
+ "@lib//:kotlinx-coroutines-core",
+ "@lib//:kotlin-stdlib",
+ "//platform/core-api:core",
+ "//platform/editor-ui-api:editor-ui",
+ "//platform/projectModel-api:projectModel",
+ "//platform/util:util-ui",
+ "//platform/diagnostic",
+ "//platform/core-ui",
+ "//platform/platform-impl:ide-impl",
+ "//platform/diff-impl",
+ "//platform/util/jdom",
+ "//platform/kernel/shared:kernel",
+ "//platform/lang-core",
+ "//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
+ "//platform/project/shared:project",
+ "//platform/util/coroutines",
+ "//libraries/kotlinx/serialization/core",
+ "//libraries/kotlinx/serialization/json",
+ "//platform/recentFiles/frontend",
+ "//platform/analysis-api:analysis",
+ "//platform/vcs-api/shared",
+ "//platform/vcs-api/vcs-api-core:vcs-core",
+ "//platform/vcs-api/vcs-api-core:vcs-core_test_lib",
+ "//platform/testFramework",
+ "//platform/testFramework:testFramework_test_lib",
+ ]
+)
+### auto-generated section `build intellij.platform.vcs.impl.frontend` end
+
+### auto-generated section `test intellij.platform.vcs.impl.frontend` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "frontend_test",
+ runtime_deps = [":frontend_test_lib"]
+)
+### auto-generated section `test intellij.platform.vcs.impl.frontend` end
\ No newline at end of file
diff --git a/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml b/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml
index 1deb1e492d98..4fc61c858384 100644
--- a/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml
+++ b/platform/vcs-impl/frontend/intellij.platform.vcs.impl.frontend.iml
@@ -30,6 +30,7 @@
+
@@ -55,5 +56,6 @@
+
\ No newline at end of file
diff --git a/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/ChangesViewDiffableSelectionHelper.kt b/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/ChangesViewDiffableSelectionHelper.kt
new file mode 100644
index 000000000000..a8cf16b35ac4
--- /dev/null
+++ b/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/ChangesViewDiffableSelectionHelper.kt
@@ -0,0 +1,110 @@
+// 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.platform.vcs.impl.frontend.changes
+
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.vcs.FilePath
+import com.intellij.openapi.vcs.changes.Change
+import com.intellij.openapi.vcs.changes.ui.ChangesBrowserNode
+import com.intellij.openapi.vcs.changes.ui.ChangesListView
+import com.intellij.openapi.vcs.changes.ui.VcsTreeModelData
+import com.intellij.platform.vcs.changes.ChangesUtil
+import com.intellij.platform.vcs.impl.shared.changes.ChangesTreePath
+import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewDiffableSelection
+import com.intellij.util.concurrency.annotations.RequiresEdt
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+private val LOG = logger()
+
+// TODO support hijacked changes
+internal class ChangesViewDiffableSelectionHelper(private val changesView: ChangesListView) {
+ private val _diffableSelection = MutableStateFlow(null)
+ val diffableSelection = _diffableSelection.asStateFlow()
+
+ @RequiresEdt
+ fun tryUpdateSelection() {
+ _diffableSelection.update { currentValue ->
+ if (shouldUpdateSelection(currentValue)) getDiffableSelection() else currentValue
+ }
+ }
+
+ @RequiresEdt
+ private fun shouldUpdateSelection(previousSelection: ChangesViewDiffableSelection?): Boolean {
+ if (previousSelection == null) return true
+
+ val currentValueIsSelected = changesView.selectedChangesNodes.any { node ->
+ when (val obj = node.userObject) {
+ is Change -> ChangesUtil.matches(obj, previousSelection.selectedChange.filePath.filePath)
+ is FilePath -> obj == previousSelection.selectedChange.filePath.filePath
+ else -> false
+ }
+ }
+
+ return !currentValueIsSelected
+ }
+
+ @RequiresEdt
+ private fun getDiffableSelection(): ChangesViewDiffableSelection? {
+ val selectedDiffableNode = changesView.selectedChanges.firstOrNull()
+ ?: changesView.selectedUnversionedFiles.firstOrNull()
+ ?: return null
+ val selectedNodePath =
+ getPathOrLog(selectedDiffableNode) { LOG.warn("Could not create path for selected node: $it") } ?: return null
+
+
+ val (prevNode, nextNode) = findPreviousAndNextDiffableNodes(selectedDiffableNode)
+
+ return ChangesViewDiffableSelection(
+ selectedChange = selectedNodePath,
+ previousChange = getPathOrLog(prevNode) { LOG.warn("Could not create path for previous node: $it") },
+ nextChange = getPathOrLog(nextNode) { LOG.warn("Could not create path for next node: $it") })
+ }
+
+ private fun getPathOrLog(prevNode: Any?, log: (Any) -> Unit): ChangesTreePath? = prevNode?.let { node ->
+ ChangesTreePath.create(node).also { path -> if (path == null) log(node) }
+ }
+
+ @RequiresEdt
+ private fun findPreviousAndNextDiffableNodes(selectedDiffableNode: Any): Pair {
+ var previousNode: Any? = null
+ var nextNode: Any? = null
+
+ var selectedNodeWasFound = false
+ for (node in VcsTreeModelData.all(changesView).iterateNodes()) {
+ if (nextNode != null) break
+
+ val userObject = node.userObject
+ if (userObject === selectedDiffableNode) {
+ selectedNodeWasFound = true
+ }
+ else {
+ if (isDiffableNode(node)) {
+ if (selectedNodeWasFound) {
+ nextNode = userObject
+ }
+ else {
+ previousNode = userObject
+ }
+ }
+ }
+ }
+
+ return previousNode to nextNode
+ }
+
+ private fun isUnderUnversioned(node: ChangesBrowserNode<*>): Boolean {
+ var current: ChangesBrowserNode<*>? = node.parent
+ while (current != null) {
+ if (current.userObject == ChangesBrowserNode.UNVERSIONED_FILES_TAG) return true
+ current = current.parent
+ }
+ return false
+ }
+
+ private fun isDiffableNode(node: ChangesBrowserNode<*>): Boolean = when (node.userObject) {
+ is Change -> true
+ is FilePath -> isUnderUnversioned(node)
+ else -> false
+ }
+}
\ No newline at end of file
diff --git a/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/FrontendCommitChangesViewWithToolbarPanel.kt b/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/FrontendCommitChangesViewWithToolbarPanel.kt
index 3ee580dae2f6..a89840822f4e 100644
--- a/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/FrontendCommitChangesViewWithToolbarPanel.kt
+++ b/platform/vcs-impl/frontend/src/com/intellij/platform/vcs/impl/frontend/changes/FrontendCommitChangesViewWithToolbarPanel.kt
@@ -13,6 +13,7 @@ import com.intellij.platform.vcs.impl.shared.changes.ChangesViewSettings
import com.intellij.platform.vcs.impl.shared.rpc.BackendChangesViewEvent
import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewApi
import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewDiffApi
+import com.intellij.util.asDisposable
import fleet.rpc.client.durable
import fleet.util.logging.logger
import kotlinx.coroutines.CoroutineScope
@@ -21,11 +22,15 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+private val LOG = logger()
+
internal class FrontendCommitChangesViewWithToolbarPanel(
changesView: ChangesListView,
cs: CoroutineScope,
val inclusionModel: ChangesViewDelegatingInclusionModel,
) : CommitChangesViewWithToolbarPanel(changesView, cs) {
+ private val diffableSelectionHelper = ChangesViewDiffableSelectionHelper(changesView)
+
init {
changesView.setInclusionModel(inclusionModel)
ChangeListsViewModel.getInstance(project).changeListsState.onEach { scheduleRefresh() }.launchIn(cs)
@@ -35,6 +40,12 @@ internal class FrontendCommitChangesViewWithToolbarPanel(
cs.launch {
forwardDiffActionsToBackend()
}
+ cs.launch {
+ diffableSelectionHelper.diffableSelection.collectLatest {
+ ChangesViewDiffApi.getInstance().notifySelectionUpdated(project.projectId(), it)
+ }
+ }
+ changesView.addSelectionListener({ diffableSelectionHelper.tryUpdateSelection() }, cs.asDisposable())
}
override fun getModelData(): ModelData {
@@ -85,8 +96,6 @@ internal class FrontendCommitChangesViewWithToolbarPanel(
}
companion object {
- private val LOG = logger()
-
fun create(project: Project, cs: CoroutineScope): FrontendCommitChangesViewWithToolbarPanel {
val tree = LocalChangesListView(project)
val inclusionModel = ChangesViewDelegatingInclusionModel(project, cs)
@@ -94,4 +103,5 @@ internal class FrontendCommitChangesViewWithToolbarPanel(
return FrontendCommitChangesViewWithToolbarPanel(tree, cs, inclusionModel)
}
}
-}
\ No newline at end of file
+}
+
diff --git a/platform/vcs-impl/frontend/testSrc/com/intellij/platform/vcs/impl/frontend/changes/ChangesViewDiffableSelectionHelperTest.kt b/platform/vcs-impl/frontend/testSrc/com/intellij/platform/vcs/impl/frontend/changes/ChangesViewDiffableSelectionHelperTest.kt
new file mode 100644
index 000000000000..5056bc7c0d95
--- /dev/null
+++ b/platform/vcs-impl/frontend/testSrc/com/intellij/platform/vcs/impl/frontend/changes/ChangesViewDiffableSelectionHelperTest.kt
@@ -0,0 +1,244 @@
+// 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.platform.vcs.impl.frontend.changes
+
+import com.intellij.openapi.vcs.FilePath
+import com.intellij.openapi.vcs.changes.Change
+import com.intellij.openapi.vcs.changes.LocalChangeListImpl
+import com.intellij.openapi.vcs.changes.ui.ChangesBrowserNode
+import com.intellij.openapi.vcs.changes.ui.VcsTreeModelData
+import com.intellij.platform.vcs.impl.changes.ChangesViewTestBase
+import com.intellij.platform.vcs.impl.shared.changes.ChangesTreePath
+import com.intellij.platform.vcs.impl.shared.rpc.ContentRevisionDto
+import com.intellij.platform.vcs.impl.shared.rpc.FilePathDto
+import com.intellij.testFramework.runInEdtAndWait
+import com.intellij.util.ui.tree.TreeUtil
+
+internal class ChangesViewDiffableSelectionHelperTest : ChangesViewTestBase() {
+ fun `test current change then next unversioned and no previous`() {
+ val changePath = path("a.txt")
+ val unversioned = path("u1.txt")
+
+ val defaultList = defaultChangeList(changePath)
+
+ val model = buildModel(view, listOf(defaultList), listOf(unversioned))
+ updateModelAndSelect(view, model, changePath)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, changePath, expectChangeId = true)
+ assertNull(selection.previousChange)
+ assertTreePath(selection.nextChange!!, unversioned, expectChangeId = false)
+ }
+
+ fun `test current change with previous and next changes`() {
+ val p1 = path("c1.txt")
+ val p2 = path("c2.txt")
+ val p3 = path("c3.txt")
+
+ val defaultList = LocalChangeListImpl.Builder(project, "Default").setDefault(true)
+ .setChanges(listOf(change(p1), change(p2), change(p3)))
+ .build()
+
+ val model = buildModel(view, listOf(defaultList), emptyList())
+ updateModelAndSelect(view, model, p2)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, p2, expectChangeId = true)
+ assertTreePath(selection.previousChange!!, p1, expectChangeId = true)
+ assertTreePath(selection.nextChange!!, p3, expectChangeId = true)
+ }
+
+ fun `test current unversioned then next unversioned and previous change`() {
+ val c1 = path("c1.txt")
+ val u1 = path("u1.txt")
+ val u2 = path("u2.txt")
+
+ val defaultList = defaultChangeList(c1)
+
+ val model = buildModel(view, listOf(defaultList), listOf(u1, u2))
+ updateModelAndSelect(view, model, u1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, u1, expectChangeId = false)
+ assertTreePath(selection.previousChange!!, c1, expectChangeId = true)
+ assertTreePath(selection.nextChange!!, u2, expectChangeId = false)
+ }
+
+ fun `test only unversioned files present`() {
+ val u1 = path("u1.txt")
+ val u2 = path("u2.txt")
+ val u3 = path("u3.txt")
+
+ val model = buildModel(view, emptyList(), listOf(u1, u2, u3))
+ updateModelAndSelect(view, model, u1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, u1, expectChangeId = false)
+ assertNull(selection.previousChange)
+ assertTreePath(selection.nextChange!!, u2, expectChangeId = false)
+ }
+
+ fun `test only current change`() {
+ val c1 = path("c1.txt")
+ val defaultList = defaultChangeList(c1)
+
+ val model = buildModel(view, listOf(defaultList), emptyList())
+ updateModelAndSelect(view, model, c1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, c1, expectChangeId = true)
+ assertNull(selection.previousChange)
+ assertNull(selection.nextChange)
+ }
+
+ fun `test only current unversioned`() {
+ val u1 = path("u1.txt")
+ val model = buildModel(view, emptyList(), listOf(u1))
+ updateModelAndSelect(view, model, u1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, u1, expectChangeId = false)
+ assertNull(selection.previousChange)
+ assertNull(selection.nextChange)
+ }
+
+ fun `test change selected then selection removed`() {
+ val c1 = path("c1.txt")
+ val c2 = path("c2.txt")
+
+ val defaultList = LocalChangeListImpl.Builder(project, "Default").setDefault(true)
+ .setChanges(listOf(change(c1), change(c2)))
+ .build()
+
+ val model = buildModel(view, listOf(defaultList), emptyList())
+ updateModelAndSelect(view, model, c1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ assertNotNull(helper.diffableSelection.value)
+
+ runInEdtAndWait {
+ view.clearSelection()
+ helper.tryUpdateSelection()
+ }
+ assertNull(helper.diffableSelection.value)
+ }
+
+
+ fun `test change selected then select changelist node doesn't cause update`() {
+ val c1 = path("c1.txt")
+ val c2 = path("c2.txt")
+
+ val defaultList = LocalChangeListImpl.Builder(project, "Default").setDefault(true)
+ .setChanges(listOf(change(c1), change(c2)))
+ .build()
+
+ val model = buildModel(view, listOf(defaultList), emptyList())
+ updateModelAndSelect(view, model, c1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val previous = helper.diffableSelection.value
+
+ // Switch selection to the changelist node
+ runInEdtAndWait {
+ val pathToList = view.findNodePathInTree(defaultList)
+ assertNotNull(pathToList)
+ view.clearSelection()
+ view.addSelectionPath(pathToList)
+ assertEquals(1, view.selectionCount)
+ helper.tryUpdateSelection()
+ }
+
+ val updated = helper.diffableSelection.value
+ assertSame(previous, updated)
+ }
+
+ fun `test unversioned selected then select unversioned root doesn't cause update`() {
+ val u1 = path("u1.txt")
+ val u2 = path("u2.txt")
+
+ val model = buildModel(view, emptyList(), listOf(u1, u2))
+ updateModelAndSelect(view, model, u1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val previous = helper.diffableSelection.value
+
+ // Switch selection to Unversioned root node
+ runInEdtAndWait {
+ val unvNode = VcsTreeModelData.findTagNode(view, ChangesBrowserNode.UNVERSIONED_FILES_TAG)
+ checkNotNull(unvNode)
+ val pathToUnv = TreeUtil.getPathFromRoot(unvNode)
+ view.clearSelection()
+ view.addSelectionPath(pathToUnv)
+ assertEquals(1, view.selectionCount)
+ helper.tryUpdateSelection()
+ }
+
+ val updated = helper.diffableSelection.value
+ assertSame(previous, updated)
+ }
+
+ fun `test switch from change to unversioned`() {
+ val c1 = path("c1.txt")
+ val u1 = path("u1.txt")
+
+ val defaultList = defaultChangeList(c1)
+ val model = buildModel(view, listOf(defaultList), listOf(u1))
+ updateModelAndSelect(view, model, c1)
+
+ val helper = ChangesViewDiffableSelectionHelper(view)
+ runInEdtAndWait { helper.tryUpdateSelection() }
+ val previous = helper.diffableSelection.value
+ checkNotNull(previous)
+ assertTreePath(previous.selectedChange, c1, true)
+
+ runInEdtAndWait {
+ val pathToU1 = view.findNodePathInTree(u1)
+ assertNotNull(pathToU1)
+ view.clearSelection()
+ view.addSelectionPath(pathToU1)
+ assertEquals(1, view.selectionCount)
+ helper.tryUpdateSelection()
+ }
+
+ val selection = helper.diffableSelection.value
+ checkNotNull(selection)
+ assertTreePath(selection.selectedChange, u1, false)
+ assertTreePath(selection.previousChange!!, c1, expectChangeId = true)
+ assertNull(selection.nextChange)
+ }
+
+ private fun defaultChangeList(changePath: FilePath): LocalChangeListImpl = LocalChangeListImpl.Builder(project, "Default")
+ .setDefault(true)
+ .setChanges(listOf(change(changePath)))
+ .build()
+
+ private fun change(path: FilePath): Change {
+ val contentRevisionDto = ContentRevisionDto("0", FilePathDto.toDto(path))
+ return Change(null, contentRevisionDto.contentRevision)
+ }
+
+ private fun assertTreePath(path: ChangesTreePath, expected: FilePath, expectChangeId: Boolean) {
+ assertEquals(expected, path.filePath.filePath)
+ if (expectChangeId) assertNotNull(path.changeId) else assertNull(path.changeId)
+ }
+}
diff --git a/platform/vcs-impl/shared/BUILD.bazel b/platform/vcs-impl/shared/BUILD.bazel
index 514469592106..6916658e0b08 100644
--- a/platform/vcs-impl/shared/BUILD.bazel
+++ b/platform/vcs-impl/shared/BUILD.bazel
@@ -41,4 +41,52 @@ jvm_library(
"//platform/diagnostic/telemetry",
]
)
-### auto-generated section `build intellij.platform.vcs.impl.shared` end
\ No newline at end of file
+
+jvm_library(
+ name = "shared_test_lib",
+ visibility = ["//visibility:public"],
+ srcs = glob(["testSrc/**/*.kt", "testSrc/**/*.java", "testSrc/**/*.form"], allow_empty = True),
+ associates = [":shared"],
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "//platform/core-api:core",
+ "//platform/kernel/shared:kernel",
+ "//platform/project/shared:project",
+ "//platform/platform-api:ide",
+ "//platform/diff-impl",
+ "//platform/projectModel-api:projectModel",
+ "//platform/util:util-ui",
+ "//libraries/kotlinx/serialization/core",
+ "//libraries/kotlinx/serialization/json",
+ "//platform/platform-impl/rpc",
+ "//platform/diagnostic",
+ "@lib//:opentelemetry",
+ "//platform/platform-impl:ide-impl",
+ "//platform/core-ui",
+ "//platform/util/coroutines",
+ "//platform/vcs-api/vcs-api-core:vcs-core",
+ "//platform/vcs-api/vcs-api-core:vcs-core_test_lib",
+ "//platform/editor-ui-api:editor-ui",
+ "//platform/lang-impl",
+ "//platform/util/concurrency",
+ "//platform/util/concurrency:concurrency_test_lib",
+ "//platform/vcs-api/shared",
+ "//libraries/fastutil",
+ "//platform/util/jdom",
+ "//platform/ide-core-impl",
+ "//platform/diagnostic/telemetry",
+ "//platform/diagnostic/telemetry:telemetry_test_lib",
+ "//platform/testFramework",
+ "//platform/testFramework:testFramework_test_lib",
+ ]
+)
+### auto-generated section `build intellij.platform.vcs.impl.shared` end
+
+### auto-generated section `test intellij.platform.vcs.impl.shared` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "shared_test",
+ runtime_deps = [":shared_test_lib"]
+)
+### auto-generated section `test intellij.platform.vcs.impl.shared` end
\ No newline at end of file
diff --git a/platform/vcs-impl/shared/intellij.platform.vcs.impl.shared.iml b/platform/vcs-impl/shared/intellij.platform.vcs.impl.shared.iml
index 30b30303b00b..040484d628f2 100644
--- a/platform/vcs-impl/shared/intellij.platform.vcs.impl.shared.iml
+++ b/platform/vcs-impl/shared/intellij.platform.vcs.impl.shared.iml
@@ -30,6 +30,7 @@
+
@@ -58,5 +59,6 @@
+
\ No newline at end of file
diff --git a/platform/vcs-impl/shared/src/com/intellij/openapi/vcs/changes/ui/ChangesTree.java b/platform/vcs-impl/shared/src/com/intellij/openapi/vcs/changes/ui/ChangesTree.java
index 663c844470a5..bb11230db38d 100644
--- a/platform/vcs-impl/shared/src/com/intellij/openapi/vcs/changes/ui/ChangesTree.java
+++ b/platform/vcs-impl/shared/src/com/intellij/openapi/vcs/changes/ui/ChangesTree.java
@@ -522,16 +522,13 @@ public abstract class ChangesTree extends Tree implements UiCompatibleDataProvid
if (node instanceof DefaultMutableTreeNode mutableTreeNode) {
Object userObject = mutableTreeNode.getUserObject();
return (userObject instanceof Change change) ?
- matches(change, toSelect) :
+ ChangesUtil.matches(change, toSelect) :
toSelect.equals(VcsTreeModelData.mapUserObjectToFilePath(userObject));
}
return false;
});
}
- private static boolean matches(@NotNull Change change, @NotNull FilePath toSelect) {
- return toSelect.equals(ChangesUtil.getAfterPath(change)) || toSelect.equals(ChangesUtil.getBeforePath(change));
- }
public @NotNull ChangesBrowserNode> getRoot() {
return (ChangesBrowserNode>)getModel().getRoot();
diff --git a/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/changes/ChangesTreePath.kt b/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/changes/ChangesTreePath.kt
index a656ab142986..cff4e42aca80 100644
--- a/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/changes/ChangesTreePath.kt
+++ b/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/changes/ChangesTreePath.kt
@@ -14,7 +14,7 @@ data class ChangesTreePath(
val filePath: FilePathDto,
val changeId: ChangeId?,
) {
- internal companion object {
+ companion object {
fun create(userObject: Any): ChangesTreePath? {
val filePath = VcsTreeModelData.mapUserObjectToFilePath(userObject) ?: return null
return ChangesTreePath(
diff --git a/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffApi.kt b/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffApi.kt
index 42c79ac330ee..0310f715d46e 100644
--- a/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffApi.kt
+++ b/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffApi.kt
@@ -14,6 +14,8 @@ import org.jetbrains.annotations.ApiStatus
interface ChangesViewDiffApi : RemoteApi {
suspend fun performDiffAction(projectId: ProjectId, action: ChangesViewDiffAction)
+ suspend fun notifySelectionUpdated(projectId: ProjectId, selection: ChangesViewDiffableSelection?)
+
companion object {
suspend fun getInstance(): ChangesViewDiffApi = RemoteApiProviderService.resolve(remoteApiDescriptor())
}
diff --git a/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffableSelection.kt b/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffableSelection.kt
new file mode 100644
index 000000000000..afd479b98535
--- /dev/null
+++ b/platform/vcs-impl/shared/src/com/intellij/platform/vcs/impl/shared/rpc/ChangesViewDiffableSelection.kt
@@ -0,0 +1,19 @@
+// 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.platform.vcs.impl.shared.rpc
+
+import com.intellij.platform.vcs.impl.shared.changes.ChangesTreePath
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Represents the currently selected change in the Changes View
+ * and the changes below and above it, which can be navigated using
+ * "Next/Previous Difference" or "Compare Previous/Next File" action
+ */
+@ApiStatus.Internal
+@Serializable
+data class ChangesViewDiffableSelection(
+ val selectedChange: ChangesTreePath,
+ val previousChange: ChangesTreePath?,
+ val nextChange: ChangesTreePath?,
+)
diff --git a/platform/vcs-impl/shared/testSrc/com/intellij/platform/vcs/impl/changes/ChangesViewTestBase.kt b/platform/vcs-impl/shared/testSrc/com/intellij/platform/vcs/impl/changes/ChangesViewTestBase.kt
new file mode 100644
index 000000000000..d7d53f4bf5fd
--- /dev/null
+++ b/platform/vcs-impl/shared/testSrc/com/intellij/platform/vcs/impl/changes/ChangesViewTestBase.kt
@@ -0,0 +1,42 @@
+// 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.platform.vcs.impl.changes
+
+import com.intellij.openapi.vcs.FilePath
+import com.intellij.openapi.vcs.actions.VcsContextFactory
+import com.intellij.openapi.vcs.changes.LocalChangeListImpl
+import com.intellij.openapi.vcs.changes.LocalChangesListView
+import com.intellij.openapi.vcs.changes.ui.ChangesTree
+import com.intellij.openapi.vcs.changes.ui.TreeModelBuilder
+import com.intellij.testFramework.LightPlatformTestCase
+import com.intellij.testFramework.runInEdtAndWait
+import javax.swing.tree.DefaultTreeModel
+
+abstract class ChangesViewTestBase : LightPlatformTestCase() {
+ protected lateinit var view: LocalChangesListView
+
+ override fun setUp() {
+ super.setUp()
+ view = LocalChangesListView(project)
+ }
+
+ protected fun updateModelAndSelect(view: LocalChangesListView, model: DefaultTreeModel, toSelect: FilePath) {
+ runInEdtAndWait {
+ view.updateTreeModel(model, ChangesTree.ALWAYS_RESET)
+ assertEquals(0, view.selectionCount)
+ view.selectFile(toSelect)
+ assertEquals(1, view.selectionCount)
+ }
+ }
+
+ protected fun path(fileName: String): FilePath =
+ VcsContextFactory.getInstance().createFilePath("/ChangesViewTest/$fileName", false)
+
+ protected fun buildModel(
+ view: LocalChangesListView,
+ lists: List,
+ unversioned: List,
+ ): DefaultTreeModel = TreeModelBuilder(project, view.grouping)
+ .setChangeLists(lists, false, null)
+ .setUnversioned(unversioned)
+ .build()
+}
\ No newline at end of file
diff --git a/platform/vcs-impl/src/com/intellij/vcs/changes/ChangesViewDiffApiImpl.kt b/platform/vcs-impl/src/com/intellij/vcs/changes/ChangesViewDiffApiImpl.kt
index 67958a388511..1aec5a95cf10 100644
--- a/platform/vcs-impl/src/com/intellij/vcs/changes/ChangesViewDiffApiImpl.kt
+++ b/platform/vcs-impl/src/com/intellij/vcs/changes/ChangesViewDiffApiImpl.kt
@@ -7,6 +7,7 @@ import com.intellij.openapi.diagnostic.trace
import com.intellij.openapi.vcs.changes.ChangesViewDiffAction
import com.intellij.platform.project.ProjectId
import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewDiffApi
+import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewDiffableSelection
import com.intellij.vcs.changes.viewModel.getRpcChangesView
import com.intellij.vcs.rpc.ProjectScopeRpcHelper.projectScoped
@@ -16,6 +17,11 @@ internal class ChangesViewDiffApiImpl : ChangesViewDiffApi {
project.getRpcChangesView().diffRequests.emit(action to ClientId.current)
}
+ override suspend fun notifySelectionUpdated(projectId: ProjectId, selection: ChangesViewDiffableSelection?) = projectScoped(projectId) { project ->
+ LOG.trace { "Selection updated for diff: $selection" }
+ project.getRpcChangesView().selectionUpdated(selection)
+ }
+
companion object {
private val LOG = logger()
}
diff --git a/platform/vcs-impl/src/com/intellij/vcs/changes/viewModel/RpcChangesViewProxy.kt b/platform/vcs-impl/src/com/intellij/vcs/changes/viewModel/RpcChangesViewProxy.kt
index 9b9b92088b0a..1eb92b1c6986 100644
--- a/platform/vcs-impl/src/com/intellij/vcs/changes/viewModel/RpcChangesViewProxy.kt
+++ b/platform/vcs-impl/src/com/intellij/vcs/changes/viewModel/RpcChangesViewProxy.kt
@@ -12,6 +12,7 @@ import com.intellij.platform.kernel.ids.BackendValueIdType
import com.intellij.platform.kernel.ids.storeValueGlobally
import com.intellij.platform.vcs.impl.shared.rpc.BackendChangesViewEvent
import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewApi
+import com.intellij.platform.vcs.impl.shared.rpc.ChangesViewDiffableSelection
import com.intellij.ui.split.createComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
@@ -42,6 +43,8 @@ internal class RpcChangesViewProxy(project: Project, scope: CoroutineScope) : Ch
val inclusionModel = MutableStateFlow(null)
+ val diffableSelection = MutableStateFlow(null)
+
private var _panel: JComponent? = null
override val panel: JComponent
get() = _panel ?: error("Panel is not initialized yet")
@@ -108,6 +111,10 @@ internal class RpcChangesViewProxy(project: Project, scope: CoroutineScope) : Ch
}
fun refreshPerformed(counter: Int) = refresher.refreshPerformed(counter)
+
+ fun selectionUpdated(selection: ChangesViewDiffableSelection?) {
+ diffableSelection.value = selection
+ }
}
private val REFRESH_TIMEOUT = 1.minutes
@@ -117,6 +124,7 @@ private class BackendRemoteCommitChangesViewModelRefresher(
private val requestsSink: MutableSharedFlow,
) {
private val refreshRequestCounter = AtomicInteger(0)
+
/**
* Once backend was notified about refresh applied with the given counter,
* all the pending callbacks having counter less than or equal to it will be executed.
diff --git a/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/changes/ui/LocalChangesListViewSelectFileTest.kt b/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/changes/ui/LocalChangesListViewSelectFileTest.kt
index 5475e7a4eb80..158fae33557e 100644
--- a/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/changes/ui/LocalChangesListViewSelectFileTest.kt
+++ b/platform/vcs-impl/testSrc/com/intellij/openapi/vcs/changes/ui/LocalChangesListViewSelectFileTest.kt
@@ -1,23 +1,19 @@
// 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.openapi.vcs.changes.ui
-import com.intellij.openapi.vcs.FilePath
-import com.intellij.openapi.vcs.actions.VcsContextFactory
-import com.intellij.openapi.vcs.changes.*
+import com.intellij.openapi.vcs.changes.Change
+import com.intellij.openapi.vcs.changes.CurrentContentRevision
+import com.intellij.openapi.vcs.changes.LocalChangeListImpl
import com.intellij.platform.vcs.changes.ChangesUtil
-import com.intellij.testFramework.LightPlatformTestCase
-import com.intellij.testFramework.runInEdtAndWait
+import com.intellij.platform.vcs.impl.changes.ChangesViewTestBase
import javax.swing.tree.DefaultMutableTreeNode
-import javax.swing.tree.DefaultTreeModel
-internal class LocalChangesListViewSelectFileTest : LightPlatformTestCase() {
+internal class LocalChangesListViewSelectFileTest : ChangesViewTestBase() {
fun `test selects unversioned file path`() {
- val view = LocalChangesListView(project)
-
- val unversionedPath = fakePath("unversioned.txt")
+ val unversionedPath = path("unversioned.txt")
val defaultList = LocalChangeListImpl.Builder(project, "Default")
.setDefault(true)
- .setChanges(listOf(Change(null, CurrentContentRevision.create(fakePath("default-change.txt")))))
+ .setChanges(listOf(Change(null, CurrentContentRevision.create(path("default-change.txt")))))
.build()
val model = buildModel(view, listOf(defaultList), listOf(unversionedPath))
@@ -27,11 +23,9 @@ internal class LocalChangesListViewSelectFileTest : LightPlatformTestCase() {
}
fun `test selects change by file path`() {
- val view = LocalChangesListView(project)
-
- val targetPath = fakePath("added.txt")
+ val targetPath = path("added.txt")
val targetChange = Change(null, CurrentContentRevision.create(targetPath))
- val anotherChange = Change(null, CurrentContentRevision.create(fakePath("another.txt")))
+ val anotherChange = Change(null, CurrentContentRevision.create(path("another.txt")))
val defaultList = LocalChangeListImpl.Builder(project, "Default")
.setDefault(true)
.setChanges(listOf(targetChange, anotherChange))
@@ -48,15 +42,13 @@ internal class LocalChangesListViewSelectFileTest : LightPlatformTestCase() {
}
fun `test selects change in multiple change lists`() {
- val view = LocalChangesListView(project)
-
- val targetPath = fakePath("default.txt")
+ val targetPath = path("default.txt")
val change = Change(null, CurrentContentRevision.create(targetPath))
val workList = LocalChangeListImpl.Builder(project, "Work").setDefault(false)
.setChanges(
listOf(
change,
- Change(null, CurrentContentRevision.create(fakePath("work-other.txt"))),
+ Change(null, CurrentContentRevision.create(path("work-other.txt"))),
)
)
.build()
@@ -74,25 +66,4 @@ internal class LocalChangesListViewSelectFileTest : LightPlatformTestCase() {
val changelistNode = view.selectionPath!!.path[1] as DefaultMutableTreeNode
assertEquals("Selected change must be under the Default changelist node", defaultList, changelistNode.userObject)
}
-
- private fun buildModel(
- view: LocalChangesListView,
- changeLists: List,
- unversionedFiles: List,
- ): DefaultTreeModel = TreeModelBuilder(project, view.grouping)
- .setChangeLists(changeLists, false, null)
- .setUnversioned(unversionedFiles)
- .build()
-
- private fun updateModelAndSelect(view: LocalChangesListView, model: DefaultTreeModel, pathToSelect: FilePath) {
- runInEdtAndWait {
- view.updateTreeModel(model, ChangesTree.ALWAYS_RESET)
- assertEquals(0, view.selectionCount)
- view.selectFile(pathToSelect)
- assertEquals(1, view.selectionCount)
- }
- }
-
- private fun fakePath(fileName: String): FilePath =
- VcsContextFactory.getInstance().createFilePath("/LocalChangesListViewSelectFileTest/$fileName", false)
}
diff --git a/platform/vcs-log/impl/BUILD.bazel b/platform/vcs-log/impl/BUILD.bazel
index 713d1ce43749..288e1fb5e52d 100644
--- a/platform/vcs-log/impl/BUILD.bazel
+++ b/platform/vcs-log/impl/BUILD.bazel
@@ -119,6 +119,7 @@ jvm_library(
"//platform/util/coroutines",
"//platform/util/storages",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//libraries/mockito",
"//platform/bookmarks",
]
diff --git a/platform/vcs-tests/BUILD.bazel b/platform/vcs-tests/BUILD.bazel
index 6891fe7c9c3d..6c3ba620a993 100644
--- a/platform/vcs-tests/BUILD.bazel
+++ b/platform/vcs-tests/BUILD.bazel
@@ -76,6 +76,7 @@ jvm_library(
"//tools/intellij.tools.ide.metrics.benchmark:ide-metrics-benchmark",
"//tools/intellij.tools.ide.metrics.benchmark:ide-metrics-benchmark_test_lib",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/core-impl",
],
exports = ["//platform/vcs-tests:vcs-testFramework"]
diff --git a/plugins/git-features-trainer/BUILD.bazel b/plugins/git-features-trainer/BUILD.bazel
index da608cd79388..bce879f1260f 100644
--- a/plugins/git-features-trainer/BUILD.bazel
+++ b/plugins/git-features-trainer/BUILD.bazel
@@ -74,6 +74,7 @@ jvm_library(
"//platform/testFramework:testFramework_test_lib",
"//platform/dvcs-impl/shared",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
]
)
### auto-generated section `build intellij.vcs.git.featuresTrainer` end
diff --git a/plugins/git4idea/BUILD.bazel b/plugins/git4idea/BUILD.bazel
index dc1a7ab7b525..cbc761cda880 100644
--- a/plugins/git4idea/BUILD.bazel
+++ b/plugins/git4idea/BUILD.bazel
@@ -145,6 +145,7 @@ jvm_library(
"//platform/vcs-impl",
"//platform/vcs-impl:vcs-impl_test_lib",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/lang-api:lang",
"@lib//:junit4",
"//platform/lvcs-api:lvcs",
diff --git a/plugins/git4idea/intellij.vcs.git.coverage/BUILD.bazel b/plugins/git4idea/intellij.vcs.git.coverage/BUILD.bazel
index 94ae77d63a57..cda1e9d29c0f 100644
--- a/plugins/git4idea/intellij.vcs.git.coverage/BUILD.bazel
+++ b/plugins/git4idea/intellij.vcs.git.coverage/BUILD.bazel
@@ -57,6 +57,7 @@ jvm_library(
"//platform/vcs-log/graph:vcs-log-graph-impl_test_lib",
"//platform/vcs-api/shared",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
]
)
### auto-generated section `build intellij.vcs.git.coverage` end
diff --git a/plugins/git4idea/terminal/BUILD.bazel b/plugins/git4idea/terminal/BUILD.bazel
index 6a46905d5aea..ea52ed6d6d1d 100644
--- a/plugins/git4idea/terminal/BUILD.bazel
+++ b/plugins/git4idea/terminal/BUILD.bazel
@@ -52,6 +52,7 @@ jvm_library(
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/vcs-api/shared",
"@lib//:assert_j",
"//libraries/mockito",
diff --git a/plugins/github/github-core/BUILD.bazel b/plugins/github/github-core/BUILD.bazel
index a0103a463c23..80300d6882c1 100644
--- a/plugins/github/github-core/BUILD.bazel
+++ b/plugins/github/github-core/BUILD.bazel
@@ -168,6 +168,7 @@ jvm_library(
"//platform/dvcs-impl/shared",
"//platform/ide-core-impl",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
]
)
### auto-generated section `build intellij.vcs.github` end
diff --git a/plugins/gitlab/gitlab-core/BUILD.bazel b/plugins/gitlab/gitlab-core/BUILD.bazel
index c3e49ab2dc52..81bc1bc66e5d 100644
--- a/plugins/gitlab/gitlab-core/BUILD.bazel
+++ b/plugins/gitlab/gitlab-core/BUILD.bazel
@@ -139,6 +139,7 @@ jvm_library(
"//platform/dvcs-impl/shared",
"//platform/ide-core-impl",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/code-style-api:codeStyle",
]
)
diff --git a/plugins/hg4idea/BUILD.bazel b/plugins/hg4idea/BUILD.bazel
index 3d2a1b56f811..58d3fa94ad33 100644
--- a/plugins/hg4idea/BUILD.bazel
+++ b/plugins/hg4idea/BUILD.bazel
@@ -81,6 +81,7 @@ jvm_library(
"//platform/util/coroutines",
"//platform/dvcs-impl/shared",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/core-impl",
]
)
diff --git a/plugins/kotlin/idea/tests/BUILD.bazel b/plugins/kotlin/idea/tests/BUILD.bazel
index 3297a68f800f..ca606017b684 100644
--- a/plugins/kotlin/idea/tests/BUILD.bazel
+++ b/plugins/kotlin/idea/tests/BUILD.bazel
@@ -58,6 +58,7 @@ jvm_library(
"//platform/usageView-impl",
"//platform/vcs-api:vcs",
"//platform/vcs-impl/shared",
+ "//platform/vcs-impl/shared:shared_test_lib",
"//platform/workspace/storage",
"//plugins/kotlin/plugin/k1",
"//plugins/kotlin/code-insight/api",