[github] Replace old Disposable-management with coroutine-based counter (IJPL-189302)

#IJPL-189302 Fixed

GitOrigin-RevId: 67361dd051d281a52a810fc7200875636d1c1f6e
This commit is contained in:
Chris Lemaire
2025-05-26 15:42:18 +02:00
committed by intellij-monorepo-bot
parent 9ea5c2dc0f
commit 99817d899d
24 changed files with 333 additions and 223 deletions

View File

@@ -19,7 +19,7 @@ internal class GHPRTimelineFileEditor(parentCs: CoroutineScope,
.childScope("GitHub Pull Request Timeline UI", Dispatchers.EDT)
.cancelledWith(this)
private val timelineVm = projectVm.acquireTimelineViewModel(file.pullRequest, this)
private val timelineVm = projectVm.acquireTimelineViewModel(file.pullRequest, cs)
override fun getName() = GithubBundle.message("pull.request.editor.timeline")

View File

@@ -3,11 +3,14 @@ package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.plugins.github.api.util.SimpleGHGQLPagesLoader
internal class GHGQLPagedListLoader<T>(progressManager: ProgressManager,
private val loader: SimpleGHGQLPagesLoader<T>)
: GHListLoaderBase<T>(progressManager) {
internal class GHGQLPagedListLoader<T>(
parentCs: CoroutineScope,
progressManager: ProgressManager,
private val loader: SimpleGHGQLPagesLoader<T>,
) : GHListLoaderBase<T>(parentCs, progressManager) {
override fun canLoadMore() = !loading && (loader.hasNext || error != null)

View File

@@ -5,7 +5,7 @@ import com.intellij.openapi.Disposable
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.util.*
interface GHListLoader<T> : Disposable {
interface GHListLoader<T> {
@get:RequiresEdt
val loading: Boolean

View File

@@ -8,18 +8,34 @@ import com.intellij.collaboration.ui.SimpleEventListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Disposer
import com.intellij.util.EventDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.plugins.github.util.NonReusableEmptyProgressIndicator
import java.util.concurrent.CompletableFuture
import kotlin.properties.Delegates
internal abstract class GHListLoaderBase<T>(private val progressManager: ProgressManager)
: GHListLoader<T> {
internal abstract class GHListLoaderBase<T>(
private val parentCs: CoroutineScope,
private val progressManager: ProgressManager
) : GHListLoader<T> {
private var lastFuture = CompletableFuture.completedFuture(emptyList<T>())
private var progressIndicator = NonReusableEmptyProgressIndicator()
init {
parentCs.launch {
try {
awaitCancellation()
}
finally {
progressIndicator.cancel()
}
}
}
private val loadingStateChangeEventDispatcher = EventDispatcher.create(SimpleEventListener::class.java)
override var loading: Boolean by Delegates.observable(false) { _, _, _ ->
loadingStateChangeEventDispatcher.multicaster.eventOccurred()
@@ -36,7 +52,7 @@ internal abstract class GHListLoaderBase<T>(private val progressManager: Progres
override fun canLoadMore() = !loading && error == null
override fun loadMore(update: Boolean) {
if (Disposer.isDisposed(this)) return
if (!parentCs.isActive) return
val indicator = progressIndicator
if (canLoadMore() || update) {
@@ -108,6 +124,4 @@ internal abstract class GHListLoaderBase<T>(private val progressManager: Progres
override fun addErrorChangeListener(disposable: Disposable, listener: () -> Unit) =
SimpleEventListener.addDisposableListener(errorChangeEventDispatcher, disposable, listener)
override fun dispose() = progressIndicator.cancel()
}

View File

@@ -6,11 +6,8 @@ import com.intellij.collaboration.async.launchNow
import com.intellij.collaboration.async.withInitial
import com.intellij.collaboration.ui.html.AsyncHtmlImageLoader
import com.intellij.collaboration.ui.icon.IconsProvider
import com.intellij.openapi.util.Disposer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.github.api.data.GHReactionContent
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequest
@@ -33,8 +30,6 @@ class GHPRDataContext internal constructor(
internal val reactionIconsProvider: IconsProvider<GHReactionContent>,
internal val interactionState: GHPRPersistentInteractionState,
) {
private val listenersDisposable = Disposer.newDisposable("GH PR context listeners disposable")
init {
scope.launchNow {
listLoader.refreshOrReloadRequests.withInitial(Unit).collectScoped {
@@ -48,22 +43,10 @@ class GHPRDataContext internal constructor(
}
}
dataProviderRepository.addDetailsLoadedListener(listenersDisposable) { details: GHPullRequest ->
dataProviderRepository.addDetailsLoadedListener(scope) { details: GHPullRequest ->
listLoader.updateData {
if (it.id == details.id) details else null
}
}
// need immediate to dispose in time
scope.launch(Dispatchers.Main.immediate) {
try {
awaitCancellation()
}
finally {
Disposer.dispose(listenersDisposable)
Disposer.dispose(dataProviderRepository)
Disposer.dispose(listUpdatesChecker)
}
}
}
}

View File

@@ -87,7 +87,7 @@ internal class GHPRDataContextRepository(private val project: Project, parentCs:
if (it is HttpStatusErrorException)
// github.com is always expected to have a ghost user, but any enterprise server may not
if (account.server.isGithubDotCom) error("Couldn't load ghost user details")
if (account.server.isGithubDotCom) error("Couldn't load ghost user details")
GHUser.FAKE_GHOST
}
@@ -140,7 +140,7 @@ internal class GHPRDataContextRepository(private val project: Project, parentCs:
val reactionsService = GHReactionsServiceImpl(requestExecutor, apiRepositoryCoordinates)
val listLoader = GHPRListLoader(cs, requestExecutor, apiRepositoryCoordinates)
val listUpdatesChecker = GHPRListETagUpdateChecker(ProgressManager.getInstance(), requestExecutor, account.server, apiRepositoryPath)
val listUpdatesChecker = GHPRListETagUpdateChecker(cs, ProgressManager.getInstance(), requestExecutor, account.server, apiRepositoryPath)
val dataProviderRepository = GHPRDataProviderRepositoryImpl(cs,
repoDataService,
@@ -149,12 +149,14 @@ internal class GHPRDataContextRepository(private val project: Project, parentCs:
filesService,
commentService,
changesService) { id ->
GHGQLPagedListLoader(ProgressManager.getInstance(),
SimpleGHGQLPagesLoader(requestExecutor, { p ->
GHGQLRequests.PullRequest.Timeline.items(account.server, apiRepositoryPath.owner,
apiRepositoryPath.repository,
id.number, p)
}, true))
GHGQLPagedListLoader(
this,
ProgressManager.getInstance(),
SimpleGHGQLPagesLoader(requestExecutor, { p ->
GHGQLRequests.PullRequest.Timeline.items(account.server, apiRepositoryPath.owner,
apiRepositoryPath.repository,
id.number, p)
}, true))
}
val interactionState = project.service<GHPRPersistentInteractionState>()

View File

@@ -1,18 +1,18 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.openapi.Disposable
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.plugins.github.api.data.pullrequest.GHPullRequest
import org.jetbrains.plugins.github.pullrequest.data.provider.GHPRDataProvider
internal interface GHPRDataProviderRepository : Disposable {
internal interface GHPRDataProviderRepository {
@RequiresEdt
fun getDataProvider(id: GHPRIdentifier, disposable: Disposable): GHPRDataProvider
fun getDataProvider(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRDataProvider
@RequiresEdt
fun findDataProvider(id: GHPRIdentifier): GHPRDataProvider?
@RequiresEdt
fun addDetailsLoadedListener(disposable: Disposable, listener: (GHPullRequest) -> Unit)
fun addDetailsLoadedListener(hostCs: CoroutineScope, listener: (GHPullRequest) -> Unit)
}

View File

@@ -1,15 +1,13 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.collaboration.async.cancelledWith
import com.intellij.collaboration.async.launchNow
import com.intellij.collaboration.async.nestedDisposable
import com.intellij.collaboration.util.getOrNull
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.CheckedDisposable
import com.intellij.openapi.util.Disposer
import com.intellij.platform.util.coroutines.childScope
import com.intellij.util.EventDispatcher
import com.intellij.util.asDisposable
import com.intellij.util.asSafely
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.messages.MessageBusFactory
@@ -29,7 +27,7 @@ import org.jetbrains.plugins.github.api.data.pullrequest.timeline.GHPRTimelineIt
import org.jetbrains.plugins.github.pullrequest.data.provider.*
import org.jetbrains.plugins.github.pullrequest.data.service.*
import org.jetbrains.plugins.github.pullrequest.ui.details.model.GHPRBranchesViewModel.Companion.getHeadRemoteDescriptor
import org.jetbrains.plugins.github.util.DisposalCountingHolder
import org.jetbrains.plugins.github.util.AcquirableScopedValueOwner
import java.util.*
internal class GHPRDataProviderRepositoryImpl(
@@ -41,48 +39,33 @@ internal class GHPRDataProviderRepositoryImpl(
private val commentService: GHPRCommentService,
private val changesService: GHPRChangesService,
private val timelineLoaderFactory: (GHPRIdentifier) -> GHListLoader<GHPRTimelineItem>,
)
: GHPRDataProviderRepository {
) : GHPRDataProviderRepository {
private val cs = parentCs.childScope(javaClass.name)
private var isDisposed = false
private val cache = mutableMapOf<GHPRIdentifier, DisposalCountingHolder<GHPRDataProvider>>()
private val cache = mutableMapOf<GHPRIdentifier, AcquirableScopedValueOwner<GHPRDataProvider>>()
private val providerDetailsLoadedEventDispatcher = EventDispatcher.create(DetailsLoadedListener::class.java)
@RequiresEdt
override fun getDataProvider(id: GHPRIdentifier, disposable: Disposable): GHPRDataProvider {
if (isDisposed) throw IllegalStateException("Already disposed")
return cache.getOrPut(id) {
DisposalCountingHolder {
createDataProvider(it, id)
}.also {
Disposer.register(it, Disposable { cache.remove(id) })
override fun getDataProvider(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRDataProvider =
cache.getOrPut(id) {
AcquirableScopedValueOwner(cs) {
createDataProvider(id)
}
}.acquireValue(disposable)
}
}.acquireValue(hostCs)
@RequiresEdt
override fun findDataProvider(id: GHPRIdentifier): GHPRDataProvider? = cache[id]?.value
override fun dispose() {
isDisposed = true
cache.values.toList().forEach(Disposer::dispose)
}
private fun createDataProvider(parentDisposable: CheckedDisposable, id: GHPRIdentifier): GHPRDataProvider {
val providerCs = cs.childScope(GHPRDataProviderImpl::class.java.name).apply {
cancelledWith(parentDisposable)
}
val messageBus = MessageBusFactory.newMessageBus(object : MessageBusOwner {
override fun isDisposed() = parentDisposable.isDisposed
private fun CoroutineScope.createDataProvider(id: GHPRIdentifier): GHPRDataProvider {
val cs = this
val messageBus = MessageBusFactory.newMessageBus (object : MessageBusOwner {
override fun isDisposed() = !cs.isActive
override fun createListener(descriptor: PluginListenerDescriptor) =
throw UnsupportedOperationException()
})
Disposer.register(parentDisposable, messageBus)
}).also { Disposer.register(cs.asDisposable(), it) }
val providerCs = cs.childScope(GHPRDataProviderImpl::class.java.name)
val detailsData = GHPRDetailsDataProviderImpl(providerCs, detailsService, id, messageBus)
providerCs.launchNow(Dispatchers.Main) {
detailsData.detailsComputationFlow.mapNotNull { it.getOrNull() }.collect {
@@ -107,9 +90,10 @@ internal class GHPRDataProviderRepositoryImpl(
}
}
val timelineLoaderHolder = DisposalCountingHolder { timelineDisposable ->
val timelineLoaderHolder = AcquirableScopedValueOwner(providerCs) {
val cs = this
timelineLoaderFactory(id).also { loader ->
messageBus.connect(timelineDisposable).subscribe(GHPRDataOperationsListener.TOPIC, object : GHPRDataOperationsListener {
messageBus.connect(cs).subscribe(GHPRDataOperationsListener.TOPIC, object : GHPRDataOperationsListener {
override fun onMetadataChanged() = loader.loadMore(true)
override fun onCommentAdded() = loader.loadMore(true)
@@ -134,10 +118,7 @@ internal class GHPRDataProviderRepositoryImpl(
loader.loadMore(true)
}
})
Disposer.register(timelineDisposable, loader)
}
}.also {
Disposer.register(parentDisposable, it)
}
messageBus.connect(providerCs.nestedDisposable()).subscribe(GHPRDataOperationsListener.TOPIC, object : GHPRDataOperationsListener {
@@ -153,12 +134,12 @@ internal class GHPRDataProviderRepositoryImpl(
)
}
override fun addDetailsLoadedListener(disposable: Disposable, listener: (GHPullRequest) -> Unit) {
override fun addDetailsLoadedListener(hostCs: CoroutineScope, listener: (GHPullRequest) -> Unit) {
providerDetailsLoadedEventDispatcher.addListener(object : DetailsLoadedListener {
override fun onDetailsLoaded(details: GHPullRequest) {
listener(details)
}
}, disposable)
}, hostCs.asDisposable())
}
private interface DetailsLoadedListener : EventListener {

View File

@@ -10,6 +10,9 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.EventDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jetbrains.plugins.github.api.GHRepositoryPath
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
import org.jetbrains.plugins.github.api.GithubApiRequests
@@ -21,6 +24,7 @@ import java.util.concurrent.TimeUnit
internal class GHPRListETagUpdateChecker(
parentCs: CoroutineScope,
private val progressManager: ProgressManager,
private val requestExecutor: GithubApiRequestExecutor,
private val serverPath: GithubServerPath,
@@ -38,6 +42,13 @@ internal class GHPRListETagUpdateChecker(
private var scheduler: ScheduledFuture<*>? = null
private var progressIndicator: ProgressIndicator? = null
init {
parentCs.launch(Dispatchers.Main.immediate) {
scheduler?.cancel(true)
progressIndicator?.cancel()
}
}
@Volatile
private var lastETag: String? = null
set(value) {
@@ -78,12 +89,6 @@ internal class GHPRListETagUpdateChecker(
lastETag = null
}
override fun dispose() {
scheduler?.cancel(true)
progressIndicator?.cancel()
}
override fun addOutdatedStateChangeListener(disposable: Disposable, listener: () -> Unit) =
SimpleEventListener.addDisposableListener(outdatedEventDispatcher, disposable, listener)
}

View File

@@ -4,8 +4,7 @@ package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.openapi.Disposable
import com.intellij.util.concurrency.annotations.RequiresEdt
internal interface GHPRListUpdatesChecker : Disposable {
internal interface GHPRListUpdatesChecker {
@get:RequiresEdt
val outdated: Boolean

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data.provider
import com.intellij.openapi.Disposable
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.plugins.github.api.data.pullrequest.timeline.GHPRTimelineItem
import org.jetbrains.plugins.github.pullrequest.data.GHListLoader
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
@@ -13,7 +13,6 @@ interface GHPRDataProvider {
val commentsData: GHPRCommentsDataProvider
val reviewData: GHPRReviewDataProvider
val viewedStateData: GHPRViewedStateDataProvider
val timelineLoader: GHListLoader<GHPRTimelineItem>?
fun acquireTimelineLoader(disposable: Disposable): GHListLoader<GHPRTimelineItem>
fun acquireTimelineLoader(hostCs: CoroutineScope): GHListLoader<GHPRTimelineItem>
}

View File

@@ -1,11 +1,11 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data.provider
import com.intellij.openapi.Disposable
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.plugins.github.api.data.pullrequest.timeline.GHPRTimelineItem
import org.jetbrains.plugins.github.pullrequest.data.GHListLoader
import org.jetbrains.plugins.github.pullrequest.data.GHPRIdentifier
import org.jetbrains.plugins.github.util.DisposalCountingHolder
import org.jetbrains.plugins.github.util.AcquirableScopedValueOwner
internal class GHPRDataProviderImpl(override val id: GHPRIdentifier,
override val detailsData: GHPRDetailsDataProvider,
@@ -13,11 +13,8 @@ internal class GHPRDataProviderImpl(override val id: GHPRIdentifier,
override val commentsData: GHPRCommentsDataProvider,
override val reviewData: GHPRReviewDataProvider,
override val viewedStateData: GHPRViewedStateDataProvider,
private val timelineLoaderHolder: DisposalCountingHolder<GHListLoader<GHPRTimelineItem>>)
private val timelineLoaderHolder: AcquirableScopedValueOwner<GHListLoader<GHPRTimelineItem>>)
: GHPRDataProvider {
override val timelineLoader get() = timelineLoaderHolder.value
override fun acquireTimelineLoader(disposable: Disposable) =
timelineLoaderHolder.acquireValue(disposable)
override fun acquireTimelineLoader(hostCs: CoroutineScope) =
timelineLoaderHolder.acquireValue(hostCs)
}

View File

@@ -6,7 +6,6 @@ import com.intellij.collaboration.async.withInitial
import com.intellij.collaboration.util.ComputedResult
import com.intellij.collaboration.util.computeEmitting
import com.intellij.collaboration.util.onFailure
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
@@ -31,7 +30,7 @@ import org.jetbrains.plugins.github.pullrequest.ui.review.GHPRBranchWidgetViewMo
import org.jetbrains.plugins.github.pullrequest.ui.timeline.GHPRTimelineViewModel
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRCreateViewModel
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.model.GHPRInfoViewModel
import org.jetbrains.plugins.github.util.DisposalCountingHolder
import org.jetbrains.plugins.github.util.AcquirableScopedValueOwner
import org.jetbrains.plugins.github.util.GHHostedRepositoriesManager
@ApiStatus.Internal
@@ -43,13 +42,13 @@ interface GHPRConnectedProjectViewModel {
fun getCreateVmOrNull(): GHPRCreateViewModel?
fun acquireAIReviewViewModel(id: GHPRIdentifier, disposable: Disposable): StateFlow<GHPRAIReviewViewModel?>
fun acquireAISummaryViewModel(id: GHPRIdentifier, disposable: Disposable): StateFlow<GHPRAISummaryViewModel?>
fun acquireInfoViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRInfoViewModel
fun acquireEditorReviewViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRReviewInEditorViewModel
fun acquireBranchWidgetModel(id: GHPRIdentifier, disposable: Disposable): GHPRBranchWidgetViewModel
fun acquireDiffViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRDiffViewModel
fun acquireTimelineViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRTimelineViewModel
fun acquireAIReviewViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): StateFlow<GHPRAIReviewViewModel?>
fun acquireAISummaryViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): StateFlow<GHPRAISummaryViewModel?>
fun acquireInfoViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRInfoViewModel
fun acquireEditorReviewViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRReviewInEditorViewModel
fun acquireBranchWidgetModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRBranchWidgetViewModel
fun acquireDiffViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRDiffViewModel
fun acquireTimelineViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRTimelineViewModel
fun findDetails(id: GHPRIdentifier): GHPullRequestShort?
@@ -76,35 +75,37 @@ abstract class GHPRConnectedProjectViewModelBase(
override val listVm: GHPRListViewModel = GHPRListViewModel(project, cs, connection.dataContext)
private val repoManager: GHHostedRepositoriesManager = project.service<GHHostedRepositoriesManager>()
private val pullRequestsVms = Caffeine.newBuilder().build<GHPRIdentifier, DisposalCountingHolder<GHPRViewModelContainer>> { id ->
DisposalCountingHolder {
GHPRViewModelContainerImpl(project, cs, dataContext, id, it, ::viewPullRequest, ::viewPullRequest, ::openPullRequestDiff,
::refreshPrOnCurrentBranch)
private val pullRequestsVms = Caffeine.newBuilder().build<GHPRIdentifier, AcquirableScopedValueOwner<GHPRViewModelContainer>> { id ->
AcquirableScopedValueOwner(cs) {
GHPRViewModelContainerImpl(
project, this, dataContext, id,
::viewPullRequest, ::viewPullRequest, ::openPullRequestDiff, ::refreshPrOnCurrentBranch
)
}
}
@ApiStatus.Internal
override fun acquireAIReviewViewModel(id: GHPRIdentifier, disposable: Disposable): StateFlow<GHPRAIReviewViewModel?> =
pullRequestsVms[id].acquireValue(disposable).aiReviewVm
override fun acquireAIReviewViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): StateFlow<GHPRAIReviewViewModel?> =
pullRequestsVms[id].acquireValue(hostCs).aiReviewVm
override fun acquireAISummaryViewModel(id: GHPRIdentifier, disposable: Disposable): StateFlow<GHPRAISummaryViewModel?> =
pullRequestsVms[id].acquireValue(disposable).aiSummaryVm
override fun acquireAISummaryViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): StateFlow<GHPRAISummaryViewModel?> =
pullRequestsVms[id].acquireValue(hostCs).aiSummaryVm
override fun acquireInfoViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRInfoViewModel =
pullRequestsVms[id].acquireValue(disposable).infoVm
override fun acquireInfoViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRInfoViewModel =
pullRequestsVms[id].acquireValue(hostCs).infoVm
override fun acquireEditorReviewViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRReviewInEditorViewModel =
pullRequestsVms[id].acquireValue(disposable).editorVm
override fun acquireEditorReviewViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRReviewInEditorViewModel =
pullRequestsVms[id].acquireValue(hostCs).editorVm
override fun acquireBranchWidgetModel(id: GHPRIdentifier, disposable: Disposable): GHPRBranchWidgetViewModel =
pullRequestsVms[id].acquireValue(disposable).branchWidgetVm
override fun acquireBranchWidgetModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRBranchWidgetViewModel =
pullRequestsVms[id].acquireValue(hostCs).branchWidgetVm
@ApiStatus.Internal
override fun acquireDiffViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRDiffViewModel =
pullRequestsVms[id].acquireValue(disposable).diffVm
override fun acquireDiffViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRDiffViewModel =
pullRequestsVms[id].acquireValue(hostCs).diffVm
override fun acquireTimelineViewModel(id: GHPRIdentifier, disposable: Disposable): GHPRTimelineViewModel =
pullRequestsVms[id].acquireValue(disposable).timelineVm
override fun acquireTimelineViewModel(id: GHPRIdentifier, hostCs: CoroutineScope): GHPRTimelineViewModel =
pullRequestsVms[id].acquireValue(hostCs).timelineVm
override fun findDetails(id: GHPRIdentifier): GHPullRequestShort? =
dataContext.listLoader.loadedData.value.find { it.id == id.id }

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2025 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
import com.intellij.collaboration.async.cancelledWith
import com.intellij.collaboration.async.collectScoped
import com.intellij.collaboration.async.launchNow
import com.intellij.collaboration.async.mapScoped
@@ -9,7 +8,6 @@ import com.intellij.collaboration.ui.codereview.details.model.CodeReviewChangeLi
import com.intellij.collaboration.ui.util.selectedItem
import com.intellij.collaboration.util.ChangesSelection
import com.intellij.collaboration.util.getOrNull
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.CoroutineScope
@@ -54,15 +52,14 @@ internal class GHPRViewModelContainerImpl(
parentCs: CoroutineScope,
dataContext: GHPRDataContext,
private val pullRequestId: GHPRIdentifier,
cancelWith: Disposable,
private val viewPullRequest: (GHPRIdentifier) -> Unit,
private val viewPullRequestOnCommit: (GHPRIdentifier, String) -> Unit,
private val openPullRequestDiff: (GHPRIdentifier?, Boolean) -> Unit,
private val refreshPrOnCurrentBranch: () -> Unit,
) : GHPRViewModelContainer {
private val cs = parentCs.childScope(javaClass.name).cancelledWith(cancelWith)
private val cs = parentCs.childScope(javaClass.name)
private val dataProvider: GHPRDataProvider = dataContext.dataProviderRepository.getDataProvider(pullRequestId, cancelWith)
private val dataProvider: GHPRDataProvider = dataContext.dataProviderRepository.getDataProvider(pullRequestId, cs)
private val diffSelectionRequests = MutableSharedFlow<ChangesSelection>(1)

View File

@@ -18,7 +18,6 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.diff.impl.GenericDataProvider
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vcs.changes.ui.PresentableChange
@@ -132,10 +131,7 @@ private fun findDiffVm(project: Project, repository: GHRepositoryCoordinates): F
} ?: flowOf(null)
private fun GHPRConnectedProjectViewModel.getDiffViewModelFlow(pullRequest: GHPRIdentifier): Flow<GHPRDiffViewModel> = channelFlow {
val acquisitionDisposable = Disposer.newDisposable()
val vm = acquireDiffViewModel(pullRequest, acquisitionDisposable)
val vm = acquireDiffViewModel(pullRequest, this)
trySend(vm)
awaitClose {
Disposer.dispose(acquisitionDisposable)
}
awaitClose()
}

View File

@@ -4,7 +4,6 @@ package org.jetbrains.plugins.github.pullrequest.ui.editor
import com.intellij.collaboration.async.collectScoped
import com.intellij.collaboration.async.launchNow
import com.intellij.collaboration.async.mapScoped
import com.intellij.collaboration.async.nestedDisposable
import com.intellij.collaboration.ui.codereview.diff.DiscussionsViewOption
import com.intellij.collaboration.ui.codereview.editor.*
import com.intellij.collaboration.util.HashingUtil
@@ -51,7 +50,7 @@ internal class GHPRReviewInEditorController(private val project: Project, privat
.flatMapLatest { projectVm ->
projectVm?.prOnCurrentBranch?.mapScoped {
val id = it?.getOrNull() ?: return@mapScoped null
projectVm.acquireEditorReviewViewModel(id, nestedDisposable())
projectVm.acquireEditorReviewViewModel(id, this)
} ?: flowOf(null)
}.collectLatest { reviewVm ->
reviewVm?.getViewModelFor(file)?.collectScoped { fileVm ->

View File

@@ -11,9 +11,7 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.util.use
import com.intellij.platform.util.coroutines.childScope
import git4idea.branch.GitBranchSyncStatus
import git4idea.branch.GitBranchUtil
@@ -43,13 +41,11 @@ class GHPROnCurrentBranchService(private val project: Project, parentCs: Corouti
?.distinctUntilChanged()
?.transformLatest<GHPRIdentifier?, GHPRBranchWidgetViewModel?> { prOnCurrentBranch ->
if (prOnCurrentBranch != null) {
Disposer.newDisposable().use {
val vm = projectVm.acquireBranchWidgetModel(prOnCurrentBranch, it)
supervisorScope {
vm.showUpdateErrorsIn(this)
emit(vm)
awaitCancellation()
}
supervisorScope {
val vm = projectVm.acquireBranchWidgetModel(prOnCurrentBranch, this)
vm.showUpdateErrorsIn(this)
emit(vm)
awaitCancellation()
}
}
else {

View File

@@ -2,7 +2,6 @@
package org.jetbrains.plugins.github.pullrequest.ui.timeline
import com.intellij.collaboration.async.mapState
import com.intellij.collaboration.async.nestedDisposable
import com.intellij.collaboration.ui.*
import com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil
import com.intellij.collaboration.ui.codereview.CodeReviewTimelineUIUtil
@@ -23,7 +22,6 @@ import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.HtmlBuilder
import com.intellij.openapi.util.text.HtmlChunk
@@ -33,6 +31,7 @@ import com.intellij.ui.PopupHandler
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.components.panels.ListLayout
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.asDisposable
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
@@ -61,9 +60,6 @@ internal class GHPRFileEditorComponentFactory(
private val timelineVm: GHPRTimelineViewModel,
private val initialDetails: GHPRDetailsFull,
) {
private val uiDisposable = cs.nestedDisposable()
fun create(): JComponent {
val mainPanel = Wrapper()
val loadedDetails = timelineVm.detailsVm.details
@@ -106,7 +102,7 @@ internal class GHPRFileEditorComponentFactory(
add(Wrapper().apply {
val summaryComponent = combine(
projectVm.acquireAISummaryViewModel(loadedDetails.value.id, uiDisposable),
projectVm.acquireAISummaryViewModel(loadedDetails.value.id, cs),
GHPRAISummaryExtension.singleFlow
) { summaryVm, extension ->
summaryVm?.let { extension?.createTimelineComponent(project, it) }
@@ -151,14 +147,13 @@ internal class GHPRFileEditorComponentFactory(
DataManager.registerDataProvider(mainPanel, DataProvider {
when {
PlatformDataKeys.UI_DISPOSABLE.`is`(it) -> uiDisposable
GHPRTimelineViewModel.DATA_KEY.`is`(it) -> timelineVm
else -> null
}
})
val actionManager = ActionManager.getInstance()
actionManager.getAction("Github.PullRequest.Timeline.Update").registerCustomShortcutSet(scrollPane, uiDisposable)
actionManager.getAction("Github.PullRequest.Timeline.Update").registerCustomShortcutSet(scrollPane, cs.asDisposable())
val groupId = "Github.PullRequest.Timeline.Popup"
PopupHandler.installPopupMenu(scrollPane, groupId, ActionPlaces.POPUP)

View File

@@ -31,10 +31,10 @@ import org.jetbrains.plugins.github.pullrequest.data.provider.GHPRDataProvider
import org.jetbrains.plugins.github.pullrequest.data.service.GHPRPersistentInteractionState.PRState
import org.jetbrains.plugins.github.pullrequest.ui.GHApiLoadingErrorHandler
import org.jetbrains.plugins.github.pullrequest.ui.GHLoadingErrorHandler
import org.jetbrains.plugins.github.pullrequest.ui.GHPRProjectViewModel
import org.jetbrains.plugins.github.pullrequest.ui.timeline.item.GHPRTimelineItem
import org.jetbrains.plugins.github.pullrequest.ui.timeline.item.UpdateableGHPRTimelineCommentViewModel
import org.jetbrains.plugins.github.pullrequest.ui.timeline.item.UpdateableGHPRTimelineReviewViewModel
import org.jetbrains.plugins.github.pullrequest.ui.GHPRProjectViewModel
import org.jetbrains.plugins.github.ui.avatars.GHAvatarIconsProvider
import org.jetbrains.plugins.github.api.data.pullrequest.timeline.GHPRTimelineItem as GHPRTimelineItemDTO
@@ -91,7 +91,7 @@ internal class GHPRTimelineViewModelImpl(
override val currentUser: GHUser = securityService.currentUser
override val detailsVm = GHPRDetailsTimelineViewModel(project, parentCs, dataContext, dataProvider)
private val timelineLoader = dataProvider.acquireTimelineLoader(cs.nestedDisposable())
private val timelineLoader = dataProvider.acquireTimelineLoader(cs)
override val loadingErrorHandler: GHLoadingErrorHandler =
GHApiLoadingErrorHandler(project, securityService.account, timelineLoader::reset)

View File

@@ -1,7 +1,6 @@
// 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.toolwindow.model
import com.intellij.collaboration.async.nestedDisposable
import com.intellij.collaboration.ui.toolwindow.ReviewToolwindowProjectViewModel
import com.intellij.collaboration.ui.toolwindow.ReviewToolwindowTabs
import com.intellij.collaboration.ui.toolwindow.ReviewToolwindowTabsStateHolder
@@ -51,7 +50,7 @@ class GHPRToolWindowProjectViewModel internal constructor(
override fun getCreateVmOrNull(): GHPRCreateViewModel? = lazyCreateVm.valueIfInitialized
init {
dataContext.dataProviderRepository.addDetailsLoadedListener(cs.nestedDisposable()) {
dataContext.dataProviderRepository.addDetailsLoadedListener(cs) {
filesManager.updateTimelineFilePresentation(it.prId)
}

View File

@@ -1,12 +1,9 @@
// 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.github.pullrequest.ui.toolwindow.model
import com.intellij.collaboration.async.cancelledWith
import com.intellij.collaboration.ui.toolwindow.ReviewTabViewModel
import com.intellij.openapi.Disposable
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
@@ -21,12 +18,12 @@ sealed interface GHPRToolWindowTabViewModel : ReviewTabViewModel {
class PullRequest internal constructor(parentCs: CoroutineScope,
projectVm: GHPRToolWindowProjectViewModel,
id: GHPRIdentifier)
: GHPRToolWindowTabViewModel, Disposable {
private val cs = parentCs.childScope().cancelledWith(this)
: GHPRToolWindowTabViewModel {
private val cs = parentCs.childScope(javaClass.name)
override val displayName: String = "#${id.number}"
val infoVm: GHPRInfoViewModel = projectVm.acquireInfoViewModel(id, this)
val infoVm: GHPRInfoViewModel = projectVm.acquireInfoViewModel(id, cs)
private val _focusRequests = Channel<Unit>(1)
internal val focusRequests: Flow<Unit> = _focusRequests.receiveAsFlow()
@@ -37,10 +34,6 @@ sealed interface GHPRToolWindowTabViewModel : ReviewTabViewModel {
fun selectCommit(oid: String) {
infoVm.detailsVm.value.result?.getOrNull()?.changesVm?.selectCommit(oid)
}
override fun dispose() {
cs.cancel()
}
}
@ApiStatus.Experimental

View File

@@ -0,0 +1,63 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.util
import com.intellij.collaboration.async.launchNow
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.*
class AcquirableScopedValueOwner<out T : Any>(
parentCs: CoroutineScope,
private val valueFactory: CoroutineScope.() -> T,
) {
private val cs = parentCs.childScope(javaClass.name)
private var subscriptionCount = 0
private var valueAndCs: Pair<T, CoroutineScope>? = null
val value: T? get() = valueAndCs?.first
init {
if (!cs.isActive) error("Already cancelled")
cs.launchNow {
try {
awaitCancellation()
}
finally {
synchronized(this) {
valueAndCs?.second?.cancel("Parent scope is cancelled")
valueAndCs = null
}
}
}
}
fun acquireValue(borrowCs: CoroutineScope): T =
synchronized(this) {
if (!cs.isActive) error("Already cancelled")
subscriptionCount += 1
borrowCs.launchNow {
try {
awaitCancellation()
}
finally {
releaseValue()
}
}
value ?: run {
val newCs = cs.childScope("value")
valueFactory(newCs) to newCs
}.also { this.valueAndCs = it }.first
}
private fun releaseValue() {
synchronized(this) {
subscriptionCount -= 1
if (subscriptionCount <= 0) {
valueAndCs?.second?.cancel("All host borrowing are cancelled")
valueAndCs = null
}
}
}
}

View File

@@ -1,53 +0,0 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.util
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.CheckedDisposable
import com.intellij.openapi.util.Disposer
class DisposalCountingHolder<T : Any>(private val valueFactory: (CheckedDisposable) -> T) : Disposable {
private var valueAndDisposable: Pair<T, CheckedDisposable>? = null
private var disposalCounter = 0
@get:Synchronized
val value: T? get() = valueAndDisposable?.first
@Synchronized
fun acquireValue(disposable: Disposable): T {
if (Disposer.isDisposed(this)) error("Already disposed")
val current = valueAndDisposable
val value = if (current == null) {
val newDisposable = Disposer.newCheckedDisposable()
val newValue = valueFactory(newDisposable)
valueAndDisposable = newValue to newDisposable
newValue
}
else {
current.first
}
disposalCounter++
Disposer.register(disposable, Disposable { release() })
return value
}
@Synchronized
private fun release() {
disposalCounter--
if (disposalCounter <= 0) {
disposeValue()
}
}
@Synchronized
private fun disposeValue() {
valueAndDisposable?.let { Disposer.dispose(it.second) }
valueAndDisposable = null
}
override fun dispose() {
disposeValue()
}
}

View File

@@ -0,0 +1,141 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.github.util
import com.intellij.collaboration.async.cancelAndJoinSilently
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class DisposalCountingHolderTest {
private data class CancellableData(
val cs: CoroutineScope,
)
@Test
fun `no value is created before acquiring`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
assertThat(holder.value).isNull()
cancelAndJoinSilently()
}
}
@Test
fun `acquiring the value twice doesn't recreate it`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
val host1 = childScope("host1")
val host2 = childScope("host2")
val v1 = holder.acquireValue(host1)
val v2 = holder.acquireValue(host2)
assertSame(v1, v2)
assertThat(v1.cs.isActive).isTrue()
cancelAndJoinSilently()
}
}
@Test
fun `acquiring the value twice, then cancelling one doesn't release it`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
val host1 = childScope("host1")
val host2 = childScope("host2")
holder.acquireValue(host1)
val v2 = holder.acquireValue(host2)
host1.cancelAndJoinSilently()
assertThat(v2.cs.isActive).isTrue()
assertThat(holder.value).isNotNull()
cancelAndJoinSilently()
}
}
@Test
fun `acquiring the value once, then releasing it, releases the value`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
val host1 = childScope("host1")
val v1 = holder.acquireValue(host1)
host1.cancelAndJoinSilently()
assertThat(v1.cs.isActive).isFalse()
assertThat(holder.value).isNull()
cancelAndJoinSilently()
}
}
@Test
fun `re-acquiring a previously released value, creates a new one`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
val host1 = childScope("host1")
val host2 = childScope("host2")
val v1 = holder.acquireValue(host1)
host1.cancelAndJoinSilently()
val v2 = holder.acquireValue(host2)
assertThat(v1).isNotSameAs(v2)
assertThat(v1.cs.isActive).isFalse()
assertThat(v2.cs.isActive).isTrue()
cancelAndJoinSilently()
}
}
@Test
fun `cancelling the holder scope releases the value`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
val host1 = childScope("host1")
val v1 = holder.acquireValue(host1)
holderScope.cancelAndJoinSilently()
assertThat(v1.cs.isActive).isFalse()
assertThat(holder.value).isNull()
cancelAndJoinSilently()
}
}
@Test
fun `cancelling the holder scope makes it an error to acquireValue`() = runTest {
launch {
val holderScope = this.childScope("holder")
val holder = AcquirableScopedValueOwner(holderScope) { CancellableData(this) }
holderScope.cancelAndJoinSilently()
assertThrows<Throwable> { holder.acquireValue(this) }
cancelAndJoinSilently()
}
}
}