From 3a81051ba5df23102b317690073d4e893dab2b99 Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Tue, 9 Sep 2025 16:53:54 +0200 Subject: [PATCH] [gitlab] refactor: more idiomatic viewmodel state GitOrigin-RevId: 24d8d8aaf13098503f50df7fbd81c8a0119f1b67 --- .../mergerequest/data/GitLabMergeRequest.kt | 5 + .../data/GitLabMergeRequestChanges.kt | 8 +- .../GitLabMergeRequestNotePositionMapping.kt | 1 - .../gitlab/mergerequest/data/GitLabNote.kt | 49 ++++--- .../GitLabMergeRequestChangesViewModel.kt | 2 +- .../GitLabMergeRequestReviewFlowViewModel.kt | 2 +- .../timeline/GitLabDiscussionDiffViewModel.kt | 125 +++++++++--------- ...questTimelineDiscussionComponentFactory.kt | 96 ++++++++------ 8 files changed, 156 insertions(+), 132 deletions(-) diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt index 85331c3abd9d..042463359e84 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequest.kt @@ -2,10 +2,12 @@ package org.jetbrains.plugins.gitlab.mergerequest.data import com.intellij.collaboration.async.* +import com.intellij.collaboration.util.ComputedResult import com.intellij.collaboration.util.ResultUtil.runCatchingUser import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import git4idea.GitStandardRemoteBranch +import git4idea.changes.GitBranchComparisonResult import git4idea.remote.hosting.GitRemoteBranchesUtil import git4idea.remote.hosting.changesSignalFlow import git4idea.repo.GitRepository @@ -89,6 +91,9 @@ interface GitLabMergeRequest : GitLabMergeRequestDiscussionsContainer { suspend fun reviewerRereview(reviewers: Collection) } +internal fun GitLabMergeRequest.changesComputationState(): Flow> = + computationStateFlow(changes) { it.getParsedChanges() } + internal class LoadedGitLabMergeRequest( private val project: Project, parentCs: CoroutineScope, diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestChanges.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestChanges.kt index 37dec4c44ab4..fd542ad5b86e 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestChanges.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestChanges.kt @@ -25,9 +25,9 @@ import org.jetbrains.plugins.gitlab.util.GitLabProjectMapping interface GitLabMergeRequestChanges { /** - * List of merge request commits + * Load the list of merge request commits */ - val commits: Deferred> + suspend fun getCommits(): List /** * Load and parse changes diffs @@ -57,7 +57,7 @@ class GitLabMergeRequestChangesImpl( private val glProject = projectMapping.repository - override val commits: Deferred> = cs.async { + private val commits: Deferred> = cs.async { if (glMetadata != null && glMetadata.version < GitLabVersion(14, 7)) { val initialURI = api.getMergeRequestCommitsURI(glProject, mergeRequestDetails.iid) return@async ApiPageUtil.createPagesFlowByLinkHeader(initialURI) { uri -> api.rest.loadMergeRequestCommits(uri) } @@ -72,6 +72,8 @@ class GitLabMergeRequestChangesImpl( .asReversed() } + override suspend fun getCommits(): List = commits.await() + private val parsedChanges = cs.async(start = CoroutineStart.LAZY) { loadChanges(commits.await()) } diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestNotePositionMapping.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestNotePositionMapping.kt index 6a2fdcbb52ba..d6a6860a9c17 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestNotePositionMapping.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestNotePositionMapping.kt @@ -11,7 +11,6 @@ interface GitLabMergeRequestNotePositionMapping { class Actual(val change: ChangesSelection.Precise) : GitLabMergeRequestNotePositionMapping class Outdated(val change: ChangesSelection.Precise) : GitLabMergeRequestNotePositionMapping object Obsolete : GitLabMergeRequestNotePositionMapping - class Error(val error: Throwable) : GitLabMergeRequestNotePositionMapping companion object { private val LOG = logger() diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt index 1e75e0160f4a..59c0be5f87bd 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt @@ -1,15 +1,16 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.gitlab.mergerequest.data -import com.intellij.collaboration.async.* -import com.intellij.openapi.diagnostic.logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi +import com.intellij.collaboration.async.Change +import com.intellij.collaboration.async.Deleted +import com.intellij.collaboration.async.childScope +import com.intellij.collaboration.async.mapState +import com.intellij.collaboration.util.ComputedResult +import com.intellij.collaboration.util.map +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext import org.jetbrains.plugins.gitlab.api.* import org.jetbrains.plugins.gitlab.api.dto.GitLabAwardEmojiDTO import org.jetbrains.plugins.gitlab.api.dto.GitLabMergeRequestDraftNoteRestDTO @@ -53,7 +54,7 @@ interface MutableGitLabNote : GitLabNote { interface GitLabMergeRequestNote : GitLabNote { val canReact: Boolean val position: StateFlow - val positionMapping: Flow + val positionMapping: StateFlow> val awardEmoji: StateFlow> @@ -68,8 +69,6 @@ interface GitLabMergeRequestDraftNote : GitLabMergeRequestNote, MutableGitLabNot override val resolved: StateFlow get() = MutableStateFlow(false) } -private val LOG = logger() - class MutableGitLabMergeRequestNote( parentCs: CoroutineScope, private val api: GitLabApi, @@ -96,7 +95,8 @@ class MutableGitLabMergeRequestNote( override val position: StateFlow = data.mapState(cs) { it.position?.let(GitLabNotePosition::from) } - override val positionMapping: Flow = position.mapPosition(mr).modelFlow(cs, LOG) + override val positionMapping: StateFlow> = + position.mapPosition(mr).stateIn(cs, SharingStarted.Eagerly, ComputedResult.loading()) override fun canEdit(): Boolean = true override fun canSubmit(): Boolean = false @@ -180,7 +180,8 @@ class GitLabMergeRequestDraftNoteImpl( override val awardEmoji: StateFlow> = MutableStateFlow(emptyList()) override val position: StateFlow = data.mapState(cs) { it.position.let(GitLabNotePosition::from) } - override val positionMapping: Flow = position.mapPosition(mr).modelFlow(cs, LOG) + override val positionMapping: StateFlow> = + position.mapPosition(mr).stateIn(cs, SharingStarted.Eagerly, ComputedResult.loading()) override fun canEdit(): Boolean = glMetadata != null && GitLabVersion(15, 10) <= glMetadata.version @@ -239,18 +240,24 @@ class GitLabMergeRequestDraftNoteImpl( } @OptIn(ExperimentalCoroutinesApi::class) -private fun Flow.mapPosition(mr: GitLabMergeRequest): Flow = - flatMapLatest { position -> - if (position == null) return@flatMapLatest flowOf(null) - - mr.changes.map { - try { - val allChanges = it.getParsedChanges() +private fun Flow.mapPosition(mr: GitLabMergeRequest): Flow> = + this.combineTransform(mr.changesComputationState()) { position, changesResult -> + if (position == null) { + emit(ComputedResult.success(null)) + return@combineTransform + } + try { + changesResult.map { allChanges -> GitLabMergeRequestNotePositionMapping.map(allChanges, position) + }.let { + emit(it) } - catch (e: Exception) { - GitLabMergeRequestNotePositionMapping.Error(e) - } + } + catch (ce: CancellationException) { + throw ce + } + catch (e: Exception) { + emit(ComputedResult.failure(e)) } } diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt index f4c10cb5349a..3f6003fcdcf1 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt @@ -54,7 +54,7 @@ internal class GitLabMergeRequestChangesViewModelImpl( override val reviewCommits: SharedFlow> = mergeRequest.changes - .map { it.commits.await().map { commit -> GitLabCommitViewModel(project, mergeRequest, commit) } } + .map { it.getCommits().map { commit -> GitLabCommitViewModel(project, mergeRequest, commit) } } .modelFlow(cs, LOG) override val selectedCommitIndex: SharedFlow = reviewCommits.combine(delegate.selectedCommit) { commits, sha -> diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestReviewFlowViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestReviewFlowViewModel.kt index 8fa391bd365c..e144ee2c4653 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestReviewFlowViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestReviewFlowViewModel.kt @@ -222,7 +222,7 @@ internal class GitLabMergeRequestReviewFlowViewModelImpl( val details = mergeRequest.details.first() val sourceBranch = details.sourceBranch val targetBranch = details.targetBranch - val commits = mergeRequest.changes.first().commits.await() + val commits = mergeRequest.changes.first().getCommits() val commitMessage: String? = withContext(scope.coroutineContext + Dispatchers.EDT) { val body = "* " + StringUtil.join(commits, { it.fullTitle }, "\n\n* ") val dialog = ReviewMergeCommitMessageDialog( diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabDiscussionDiffViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabDiscussionDiffViewModel.kt index 42568364690e..2ea0eca747dc 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabDiscussionDiffViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabDiscussionDiffViewModel.kt @@ -2,14 +2,16 @@ package org.jetbrains.plugins.gitlab.mergerequest.ui.timeline import com.intellij.collaboration.async.childScope -import com.intellij.collaboration.async.modelFlow import com.intellij.collaboration.ui.codereview.diff.DiffLineLocation import com.intellij.collaboration.util.ChangesSelection +import com.intellij.collaboration.util.ComputedResult import com.intellij.collaboration.util.selectedChange import com.intellij.diff.util.Side import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diff.impl.patch.PatchHunk import com.intellij.openapi.diff.impl.patch.TextFilePatch +import com.intellij.openapi.progress.checkCanceled +import git4idea.changes.GitBranchComparisonResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow @@ -18,21 +20,17 @@ import kotlinx.coroutines.launch import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestNotePositionMapping import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabNotePosition -import org.jetbrains.plugins.gitlab.mergerequest.ui.timeline.GitLabDiscussionDiffViewModel.PatchHunkResult +import org.jetbrains.plugins.gitlab.mergerequest.ui.timeline.GitLabDiscussionDiffViewModel.PatchHunkWithAnchor +import kotlin.coroutines.cancellation.CancellationException interface GitLabDiscussionDiffViewModel { val position: GitLabNotePosition - val mapping: Flow - val patchHunk: Flow + val patchHunk: StateFlow> val showDiffRequests: Flow - val showDiffHandler: Flow<(() -> Unit)?> + val showDiffHandler: StateFlow<(() -> Unit)?> - sealed interface PatchHunkResult { - class Loaded(val hunk: PatchHunk, val anchor: DiffLineLocation) : PatchHunkResult - object NotLoaded : PatchHunkResult - class Error(val error: Throwable) : PatchHunkResult - } + data class PatchHunkWithAnchor(val hunk: PatchHunk, val anchor: DiffLineLocation) } private val LOG = logger() @@ -46,41 +44,7 @@ class GitLabDiscussionDiffViewModelImpl( private val cs = parentCs.childScope(this::class) - override val mapping: Flow = mr.changes.mapLatest { - try { - val allChanges = it.getParsedChanges() - GitLabMergeRequestNotePositionMapping.map(allChanges, position) - } - catch (e: Exception) { - GitLabMergeRequestNotePositionMapping.Error(e) - } - }.modelFlow(cs, LOG) - - - override val patchHunk: Flow = channelFlow { - mr.changes.mapLatest { it.getParsedChanges() }.catch { e -> - send(PatchHunkResult.Error(e)) - }.combine(mapping) { allChanges, mapping -> - when { - mapping is GitLabMergeRequestNotePositionMapping.Actual && mapping.change.location != null -> { - val patch = allChanges.patchesByChange[mapping.change.selectedChange]?.patch ?: run { - LOG.warn("Can't find patch for ${mapping.change.selectedChange}") - return@combine PatchHunkResult.NotLoaded - } - - val (hunk, anchor) = findHunkAndAnchor(patch, mapping.change.location!!) ?: run { - LOG.debug("Unable to map location for position $position in patch\n$patch") - return@combine PatchHunkResult.NotLoaded - } - PatchHunkResult.Loaded(hunk, anchor) - } - mapping is GitLabMergeRequestNotePositionMapping.Error -> PatchHunkResult.Error(mapping.error) - else -> PatchHunkResult.NotLoaded - } - }.collectLatest { - send(it) - } - }.modelFlow(cs, LOG) + override val patchHunk: StateFlow> private val _showDiffRequests = MutableSharedFlow( extraBufferCapacity = 1, @@ -88,38 +52,77 @@ class GitLabDiscussionDiffViewModelImpl( ) override val showDiffRequests: Flow = _showDiffRequests.asSharedFlow() - override val showDiffHandler: Flow<(() -> Unit)?> = mapping.map { - when (it) { - is GitLabMergeRequestNotePositionMapping.Actual -> { - { requestFullDiff(it.change) } + override val showDiffHandler: StateFlow<(() -> Unit)?> + + init { + patchHunk = MutableStateFlow(ComputedResult.loading()) + showDiffHandler = MutableStateFlow(null) + + cs.launch { + mr.changes.collectLatest { + try { + val allChanges = it.getParsedChanges() + val mapping = GitLabMergeRequestNotePositionMapping.map(allChanges, position) + + patchHunk.value = ComputedResult.success(getPatchHunk(allChanges, mapping)) + showDiffHandler.value = when (mapping) { + is GitLabMergeRequestNotePositionMapping.Actual -> { + { + _showDiffRequests.tryEmit(mapping.change) + } + } + is GitLabMergeRequestNotePositionMapping.Outdated -> { + { + _showDiffRequests.tryEmit(mapping.change) + } + } + else -> null + } + + } + catch (@Suppress("IncorrectCancellationExceptionHandling") _: CancellationException) { + // getParsedChanges can be canceled outside + checkCanceled() + } + catch (e: Exception) { + patchHunk.value = ComputedResult.failure(e) + showDiffHandler.value = null + } } - is GitLabMergeRequestNotePositionMapping.Outdated -> { - { requestFullDiff(it.change) } - } - else -> null } } - private fun requestFullDiff(change: ChangesSelection.Precise) { - cs.launch { - _showDiffRequests.emit(change) + private fun getPatchHunk( + allChanges: GitBranchComparisonResult, + mapping: GitLabMergeRequestNotePositionMapping, + ): PatchHunkWithAnchor? { + if (mapping !is GitLabMergeRequestNotePositionMapping.Actual) return null + val location = mapping.change.location ?: return null + + val patch = allChanges.patchesByChange[mapping.change.selectedChange]?.patch ?: run { + LOG.warn("Can't find patch for ${mapping.change.selectedChange}") + return null + } + + return findHunkAndAnchor(patch, location) ?: run { + LOG.debug("Unable to map location for position $position in patch\n$patch") + null } } } -private fun findHunkAndAnchor(patch: TextFilePatch, location: DiffLineLocation): Pair? { +private fun findHunkAndAnchor(patch: TextFilePatch, location: DiffLineLocation): PatchHunkWithAnchor? { val (side, index) = location return when (side) { Side.LEFT -> { patch.hunks.find { index >= it.startLineBefore && index < it.endLineBefore - }?.let { it to location } + } } Side.RIGHT -> { patch.hunks.find { index >= it.startLineAfter && index < it.endLineAfter - }?.let { it to location } + } } - else -> null - } + }?.let { PatchHunkWithAnchor(it, location) } } diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt index dab399f455ba..5fe564531aa1 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt @@ -18,16 +18,15 @@ import com.intellij.collaboration.ui.util.DimensionRestrictions import com.intellij.collaboration.ui.util.bindChildIn import com.intellij.collaboration.ui.util.bindContentIn import com.intellij.collaboration.ui.util.bindVisibilityIn +import com.intellij.collaboration.util.exceptionOrNull +import com.intellij.collaboration.util.getOrNull import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.HtmlBuilder import com.intellij.openapi.util.text.HtmlChunk import com.intellij.ui.HyperlinkAdapter import com.intellij.ui.components.panels.Wrapper -import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.SingleComponentCenteringLayout -import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -189,56 +188,65 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory { private fun CoroutineScope.createDiffPanel(project: Project, diffVm: GitLabDiscussionDiffViewModel): JComponent = Wrapper(LoadingLabel()).apply { - bindContentIn(this@createDiffPanel, diffVm.patchHunk) { hunkState -> + bindContentIn(this@createDiffPanel, diffVm.patchHunk) { hunkResult -> val loadedDiffCs = this - when (hunkState) { - is GitLabDiscussionDiffViewModel.PatchHunkResult.Loaded -> { - TimelineDiffComponentFactory.createDiffComponentIn(loadedDiffCs, project, EditorFactory.getInstance(), hunkState.hunk, - hunkState.anchor, null) - } - is GitLabDiscussionDiffViewModel.PatchHunkResult.Error, - GitLabDiscussionDiffViewModel.PatchHunkResult.NotLoaded -> { - JPanel(SingleComponentCenteringLayout()).apply { - isOpaque = false - border = JBUI.Borders.empty(16) - bindChildIn(loadedDiffCs, diffVm.showDiffHandler.asActionHandler()) { clickListener -> - if (clickListener != null) { - val text = buildCantLoadHunkText(hunkState) - .append(HtmlChunk.p().child( - HtmlChunk.link(OPEN_DIFF_LINK_HREF, GitLabBundle.message("merge.request.timeline.discussion.open.full.diff")))) - .wrapWith(HtmlChunk.div("text-align: center")) - .toString() + if (hunkResult.isInProgress) { + return@bindContentIn LoadingLabel() + } - SimpleHtmlPane(addBrowserListener = false).apply { - setHtmlBody(text) - }.also { - it.addHyperlinkListener(object : HyperlinkAdapter() { - override fun hyperlinkActivated(e: HyperlinkEvent) { - if (e.description == OPEN_DIFF_LINK_HREF) { - clickListener.actionPerformed(ActionEvent(it, ActionEvent.ACTION_PERFORMED, "execute")) - } - } - }) - } - } - else { - val text = buildCantLoadHunkText(hunkState).toString() - SimpleHtmlPane(text) - } - } - } - } + val hunk = hunkResult.getOrNull() + if (hunk != null) { + TimelineDiffComponentFactory.createDiffComponentIn(loadedDiffCs, project, EditorFactory.getInstance(), hunk.hunk, hunk.anchor, null) + } + else { + createMissingHunkComponent(diffVm, hunkResult.exceptionOrNull()) } } } - private fun buildCantLoadHunkText(hunkState: GitLabDiscussionDiffViewModel.PatchHunkResult) = + private fun createMissingHunkComponent( + diffVm: GitLabDiscussionDiffViewModel, + error: Throwable?, + ): JPanel = JPanel(SingleComponentCenteringLayout()).apply { + isOpaque = false + border = JBUI.Borders.empty(16) + + launchOnShow("Hunk text") { + bindChildIn(this, diffVm.showDiffHandler.asActionHandler()) { clickListener -> + if (clickListener != null) { + val text = buildCantLoadHunkText(error) + .append(HtmlChunk.p().child( + HtmlChunk.link(OPEN_DIFF_LINK_HREF, GitLabBundle.message("merge.request.timeline.discussion.open.full.diff")))) + .wrapWith(HtmlChunk.div("text-align: center")) + .toString() + + SimpleHtmlPane(addBrowserListener = false).apply { + setHtmlBody(text) + }.also { + it.addHyperlinkListener(object : HyperlinkAdapter() { + override fun hyperlinkActivated(e: HyperlinkEvent) { + if (e.description == OPEN_DIFF_LINK_HREF) { + clickListener.actionPerformed(ActionEvent(it, ActionEvent.ACTION_PERFORMED, "execute")) + } + } + }) + } + } + else { + val text = buildCantLoadHunkText(error).toString() + SimpleHtmlPane(text) + } + } + } + } + + private fun buildCantLoadHunkText(error: Throwable?) = HtmlBuilder() .append(HtmlChunk.p().addText(GitLabBundle.message("merge.request.timeline.discussion.cant.load.diff"))) .apply { - if (hunkState is GitLabDiscussionDiffViewModel.PatchHunkResult.Error) { - append(HtmlChunk.p().addText(hunkState.error.localizedMessageOrClassName())) + if (error != null) { + append(HtmlChunk.p().addText(error.localizedMessageOrClassName())) } }