From ed95556209696862691b5bcbc59a4b39ee59cccc Mon Sep 17 00:00:00 2001 From: Bartosz Janusz Date: Thu, 25 Sep 2025 17:03:17 +0200 Subject: [PATCH] [gh] Add a new way of setuping multiline comment in the diff PR review by dragging frame #IJPL-200028 Fixed GitOrigin-RevId: 4ecff4edaab3488c15e6e8df6fa669313e627981 --- .../ui/diff/GHPRDiffReviewViewModel.kt | 12 ++++- .../ui/diff/GHPRNewCommentDiffViewModel.kt | 9 ++-- .../ui/diff/GHPRReviewDiffExtension.kt | 44 +++++++++++-------- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRDiffReviewViewModel.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRDiffReviewViewModel.kt index 3e0e40db0dfd..4b0cce40de90 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRDiffReviewViewModel.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRDiffReviewViewModel.kt @@ -10,6 +10,7 @@ import com.intellij.collaboration.util.ComputedResult import com.intellij.collaboration.util.RefComparisonChange import com.intellij.collaboration.util.filePath import com.intellij.collaboration.util.getOrNull +import com.intellij.diff.util.LineRange import com.intellij.diff.util.Range import com.intellij.diff.util.Side import com.intellij.openapi.diff.impl.patch.PatchHunkUtil @@ -47,6 +48,7 @@ interface GHPRDiffReviewViewModel { fun requestNewComment(location: GHPRReviewCommentLocation, focus: Boolean) fun cancelNewComment(side: Side, lineIdx: Int) + fun updateCommentLines(oldLineRange: LineRange, newLineRange: LineRange) val isViewedState: StateFlow> fun setViewedState(isViewed: Boolean) @@ -97,7 +99,7 @@ internal class GHPRDiffReviewViewModelImpl( private val newCommentsContainer = MappingScopedItemsContainer.byIdentity(cs) { - GHPRNewCommentDiffViewModelImpl(it.position.value.location, it) + GHPRNewCommentDiffViewModelImpl(it.position, it) } override val newComments: StateFlow> = newCommentsContainer.mappingState.mapState { it.values } @@ -131,6 +133,14 @@ internal class GHPRDiffReviewViewModelImpl( threadsVms.cancelNewComment(change, side, lineIdx) + override fun updateCommentLines(oldLineRange: LineRange, newLineRange: LineRange) = + threadsVms.newComments.value.firstOrNull { + when (val loc = it.position.value.location) { + is GHPRReviewCommentLocation.SingleLine -> loc.lineIdx == oldLineRange.end + is GHPRReviewCommentLocation.MultiLine -> loc.startLineIdx == oldLineRange.start && loc.lineIdx == oldLineRange.end + } + }?.updateLineRange(newLineRange) ?: Unit + override val isViewedState: StateFlow> = dataProvider.viewedStateData.viewedStateComputationState .map { viewedStateByPath -> diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRNewCommentDiffViewModel.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRNewCommentDiffViewModel.kt index 2fa66ffaba36..b250625d368a 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRNewCommentDiffViewModel.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRNewCommentDiffViewModel.kt @@ -1,25 +1,28 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.github.pullrequest.ui.diff +import com.intellij.collaboration.async.mapState import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.receiveAsFlow import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRReviewCommentLocation +import org.jetbrains.plugins.github.pullrequest.ui.comment.GHPRReviewCommentPosition import org.jetbrains.plugins.github.pullrequest.ui.editor.GHPRReviewNewCommentEditorViewModel interface GHPRNewCommentDiffViewModel : GHPRReviewNewCommentEditorViewModel { - val location: GHPRReviewCommentLocation + val location: StateFlow } internal class GHPRNewCommentDiffViewModelImpl( - override val location: GHPRReviewCommentLocation, + position: StateFlow, private val sharedVm: GHPRReviewNewCommentEditorViewModel, ) : GHPRNewCommentDiffViewModel, GHPRReviewNewCommentEditorViewModel by sharedVm { private val _focusRequestsChannel = Channel(1, BufferOverflow.DROP_OLDEST) override val focusRequests: Flow get() = _focusRequestsChannel.receiveAsFlow() - + override val location = position.mapState { it.location } override fun requestFocus() { _focusRequestsChannel.trySend(Unit) } diff --git a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt index da17efd42fcf..86a5e706d2e8 100644 --- a/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt +++ b/plugins/github/github-core/src/org/jetbrains/plugins/github/pullrequest/ui/diff/GHPRReviewDiffExtension.kt @@ -29,10 +29,8 @@ import com.intellij.util.cancelOnDispose import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import org.jetbrains.plugins.github.ai.GHPRAICommentViewModel import org.jetbrains.plugins.github.pullrequest.ui.GHPRInlayUtils @@ -137,10 +135,18 @@ private class DiffEditorModel( override val inlays: StateFlow> = combineStateIn(cs, threads, newComments, aiComments) { threads, new, ai -> threads + new + ai } + @OptIn(ExperimentalCoroutinesApi::class) + private val linesWithNewCommentsFlow: StateFlow> = + diffVm.newComments.flatMapLatest { vms -> + if (vms.isEmpty()) flowOf(emptySet()) + else combine(vms.map { it.location.map { loc -> locationToLine(loc.lineLocation) } }) { lines -> + lines.filterNotNull().toSet() + } + }.stateInNow(cs, emptySet()) + override val gutterControlsState: StateFlow = - combine(diffVm.locationsWithDiscussions, diffVm.newComments) { locationsWithDiscussions, newComments -> + combine(diffVm.locationsWithDiscussions, linesWithNewCommentsFlow) { locationsWithDiscussions, linesWithNewComments -> val linesWithComments = locationsWithDiscussions.mapNotNullTo(mutableSetOf(), locationToLine) - val linesWithNewComments = newComments.mapNotNullTo(mutableSetOf()) { locationToLine(it.location.lineLocation) } GHPRReviewEditorGutterControlsState( linesWithComments, linesWithNewComments, if (diffVm.canComment) transferRanges(diffVm.commentableRanges, diffVm.changedRanges) else emptyList() @@ -201,7 +207,7 @@ private class DiffEditorModel( } override fun updateCommentLines(oldLineRange: LineRange, newLineRange: LineRange) { - TODO("Not yet implemented") + diffVm.updateCommentLines(LineRange(oldLineRange.start, oldLineRange.end), LineRange(newLineRange.start, newLineRange.end)) } override fun requestNewComment(lineIdx: Int) { @@ -264,21 +270,21 @@ private class DiffEditorModel( private inner class MappedNewComment(vm: GHPRNewCommentDiffViewModel) : GHPREditorMappedComponentModel.NewComment(vm) { - private val location = vm.position.value.location.lineLocation.let { it.first to it.second } + private val location: StateFlow = vm.position.mapState { it.location } override val key: Any = "NEW_${vm.position.value.location}" override val isVisible: StateFlow = MutableStateFlow(true) - override val line: StateFlow = MutableStateFlow(locationToLine(location)) - override val range: StateFlow?> = MutableStateFlow(when (val loc = vm.location) { - is GHPRReviewCommentLocation.SingleLine -> { - loc.side to loc.lineIdx..loc.lineIdx - } - is GHPRReviewCommentLocation.MultiLine -> { - loc.side to loc.startLineIdx..loc.lineIdx - } - }) - + override val line: StateFlow = location.mapState { locationToLine(it.lineLocation) } + private val _range = MutableStateFlow?>(when (val loc = location.value) { + is GHPRReviewCommentLocation.SingleLine -> { + loc.side to loc.lineIdx..loc.lineIdx + } + is GHPRReviewCommentLocation.MultiLine -> { + loc.side to loc.startLineIdx..loc.lineIdx + } + }) + override val range: StateFlow?> = _range.asStateFlow() override fun setRange(range: Pair?) { - TODO("Not yet implemented") + _range.value = range } }