mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[github] Replace old Disposable-management with coroutine-based counter (IJPL-189302)
#IJPL-189302 Fixed GitOrigin-RevId: 67361dd051d281a52a810fc7200875636d1c1f6e
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9ea5c2dc0f
commit
99817d899d
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user