mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
feature (vcs): Implement API required for showing changes view diff (IJPL-173924)
It exposes currently selected diffable change in the tree and next/previous nodes. If selection is changed, but previous selection can still be considered selected, the n ew value is not reported GitOrigin-RevId: b4f649765cb31dae7d3fbba540bf7f673d2944e3
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3476cf8782
commit
3fead1c6ab
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -38,4 +38,48 @@ jvm_library(
|
||||
"//platform/vcs-api/vcs-api-core:vcs-core",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.platform.vcs.impl.frontend` end
|
||||
|
||||
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
|
||||
@@ -30,6 +30,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
@@ -55,5 +56,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.analysis" />
|
||||
<orderEntry type="module" module-name="intellij.platform.vcs.shared" />
|
||||
<orderEntry type="module" module-name="intellij.platform.vcs.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
+110
@@ -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<ChangesViewDiffableSelectionHelper>()
|
||||
|
||||
// TODO support hijacked changes
|
||||
internal class ChangesViewDiffableSelectionHelper(private val changesView: ChangesListView) {
|
||||
private val _diffableSelection = MutableStateFlow<ChangesViewDiffableSelection?>(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<Any?, Any?> {
|
||||
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
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -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<FrontendCommitChangesViewWithToolbarPanel>()
|
||||
|
||||
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<FrontendCommitChangesViewWithToolbarPanel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+244
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,52 @@ jvm_library(
|
||||
"//platform/diagnostic/telemetry",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.platform.vcs.impl.shared` end
|
||||
|
||||
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
|
||||
@@ -30,6 +30,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="library" name="kotlin-stdlib" level="project" />
|
||||
@@ -58,5 +59,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.core.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.diagnostic.telemetry" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+2
@@ -14,6 +14,8 @@ import org.jetbrains.annotations.ApiStatus
|
||||
interface ChangesViewDiffApi : RemoteApi<Unit> {
|
||||
suspend fun performDiffAction(projectId: ProjectId, action: ChangesViewDiffAction)
|
||||
|
||||
suspend fun notifySelectionUpdated(projectId: ProjectId, selection: ChangesViewDiffableSelection?)
|
||||
|
||||
companion object {
|
||||
suspend fun getInstance(): ChangesViewDiffApi = RemoteApiProviderService.resolve(remoteApiDescriptor<ChangesViewDiffApi>())
|
||||
}
|
||||
|
||||
+19
@@ -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?,
|
||||
)
|
||||
+42
@@ -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<LocalChangeListImpl>,
|
||||
unversioned: List<FilePath>,
|
||||
): DefaultTreeModel = TreeModelBuilder(project, view.grouping)
|
||||
.setChangeLists(lists, false, null)
|
||||
.setUnversioned(unversioned)
|
||||
.build()
|
||||
}
|
||||
@@ -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<ChangesViewDiffApiImpl>()
|
||||
}
|
||||
|
||||
@@ -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<InclusionModel?>(null)
|
||||
|
||||
val diffableSelection = MutableStateFlow<ChangesViewDiffableSelection?>(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<in BackendChangesViewEvent.RefreshRequested>,
|
||||
) {
|
||||
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.
|
||||
|
||||
+11
-40
@@ -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<ChangeList>,
|
||||
unversionedFiles: List<FilePath>,
|
||||
): 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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user