diff --git a/platform/platform-impl/src/com/intellij/openapi/project/impl/entities.kt b/platform/platform-impl/src/com/intellij/openapi/project/impl/entities.kt new file mode 100644 index 000000000000..c8294eb903de --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/project/impl/entities.kt @@ -0,0 +1,138 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.project.impl + +import com.intellij.openapi.observable.util.whenDisposed +import com.intellij.openapi.progress.runBlockingMaybeCancellable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.getOrCreateUserData +import com.intellij.platform.kernel.KernelService +import com.intellij.platform.kernel.util.flushLatestChange +import com.intellij.platform.kernel.withKernel +import com.jetbrains.rhizomedb.* +import fleet.kernel.DurableEntityType +import fleet.kernel.change +import fleet.kernel.kernel +import fleet.kernel.shared +import fleet.util.UID +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +val KERNEL_PROJECT_ID = Key.create("ProjectImpl.KERNEL_PROJECT_ID") + +/** + * Represents a project entity that can be shared between backend and frontends. + * The entity is created on project initialization before any services and components are loaded. + * + * To convert a project to the entity use [asEntity] + */ +@ApiStatus.Internal +data class ProjectEntity(override val eid: EID) : Entity { + var projectId: UID by ProjectId + + companion object: DurableEntityType(ProjectEntity::class.java.name, "com.intellij", ::ProjectEntity) { + val ProjectId = requiredValue("projectId", UID.serializer(), Indexing.UNIQUE) + } +} + +data class LocalProjectEntity(override val eid: EID) : Entity { + val sharedEntity: ProjectEntity by ProjectEntity + val project: Project by Project + + companion object: EntityType(LocalProjectEntity::class, ::LocalProjectEntity) { + val ProjectEntity = requiredRef("sharedEntity", RefFlags.CASCADE_DELETE_BY) + val Project = requiredTransient("project") + } +} + + +/** + * Converts a given project to its corresponding [ProjectEntity]. + * + * The method has to be called in a kernel context - see [com.intellij.platform.kernel.KernelService.kernelCoroutineContext] + * + * @return The [ProjectEntity] instance associated with the provided project, + * or null if no such entity is found + */ +@ApiStatus.Internal +fun Project.asEntity(): ProjectEntity? { + return LocalProjectEntity.all().singleOrNull { it.project == this }?.sharedEntity +} + +/** + * Converts a given project entity to its corresponding [Project]. + * + * The method has to be called in a kernel context - see [com.intellij.platform.kernel.KernelService.kernelCoroutineContext] + * + * @return The [Project] instance associated with the provided entity, + * or null if no such project is found (for example, if [ProjectEntity] doesn't exist anymore). + */ +@ApiStatus.Internal +fun ProjectEntity.asProject(): Project? { + return LocalProjectEntity.all().singleOrNull { it.sharedEntity == this }?.project +} + +internal suspend fun Project.createEntity() = withKernel { + val project = this@createEntity + val projectId = project.getOrCreateUserData(KERNEL_PROJECT_ID) { UID.random() } + + // TODO it shouldn't be here + change { + shared { + register(ProjectEntity) + } + } + + change { + val projectEntity = shared { + /* + This check is added to ensure that only one ProjectEntity is going to be created in split mode. + Two entities are possible due to a different flow in creating a project in split mode. + + First, a project is created on the backend (ProjectEntity is created at the same time). + Then a signal about project creation is sent to the frontend via RD protocol. + At the same time, the shared part of Rhizome DB (where ProjectEntity is stored) sends the changes to the frontend. + + Events which are coming via RD protocol are not synced with events coming via Rhizome DB. + So it can happen that while on the backend the signal is sent strictly after ProjectEntity creation, + on the frontend the signal can be received before there is ProjectEntity available in DB. + + If it happens that the entity has not been found and the frontend creates a new one, Rhizome DB will perform a "rebase" + which basically re-invokes the whole "change" block either on the backend or the frontend side. + */ + val existing = ProjectEntity.all().singleOrNull { it.projectId == projectId } + if (existing != null) { + existing + } + else { + ProjectEntity.new { + it[ProjectEntity.ProjectId] = projectId + } + } + } + + LocalProjectEntity.new { + it[LocalProjectEntity.ProjectEntity] = projectEntity + it[LocalProjectEntity.Project] = project + } + } + + project.whenDisposed { + runBlockingMaybeCancellable { + removeProjectEntity(project) + } + } +} + +private suspend fun removeProjectEntity(project: Project) = withKernel { + change { + shared { + project.asEntity()?.delete() + } + } + + // Removing ProjectEntity and LocalProjectEntity is the last operation in most of the tests + // Without calling "flushLatestChange" kernel keeps the project, which causes "testProjectLeak" failures + kernel().flushLatestChange() +} diff --git a/platform/xdebugger-impl/backend/src/com/intellij/xdebugger/impl/backend/BackendDebuggerValueLookupHintsRemoteApiProvider.kt b/platform/xdebugger-impl/backend/src/com/intellij/xdebugger/impl/backend/BackendDebuggerValueLookupHintsRemoteApiProvider.kt index aafb850649cb..62263c599289 100644 --- a/platform/xdebugger-impl/backend/src/com/intellij/xdebugger/impl/backend/BackendDebuggerValueLookupHintsRemoteApiProvider.kt +++ b/platform/xdebugger-impl/backend/src/com/intellij/xdebugger/impl/backend/BackendDebuggerValueLookupHintsRemoteApiProvider.kt @@ -106,7 +106,7 @@ private class BackendDebuggerValueLookupHintsRemoteApi : XDebuggerValueLookupHin } private suspend fun getProjectFromUID(projectId: UID): Project? { - return withContext(KernelService.kernelCoroutineContext()) { + return withKernel { val projectEntity = entity(ProjectEntity.ProjectId, projectId) projectEntity?.asProject() } diff --git a/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupManagerSubscriptions.kt b/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupManagerSubscriptions.kt index 2a444c8d7f73..7c070faa9151 100644 --- a/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupManagerSubscriptions.kt +++ b/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupManagerSubscriptions.kt @@ -2,6 +2,7 @@ package com.intellij.xdebugger.impl.frontend.evaluate.quick.common import com.intellij.platform.kernel.KernelService +import com.intellij.platform.kernel.withKernel import com.intellij.xdebugger.impl.evaluate.XDebuggerValueLookupHideHintsRequestEntity import com.intellij.xdebugger.impl.evaluate.XDebuggerValueLookupListeningStartedEntity import fleet.kernel.change @@ -15,7 +16,7 @@ import kotlinx.coroutines.withContext internal fun subscribeForDebuggingStart(cs: CoroutineScope, onStartListening: () -> Unit) { cs.launch(Dispatchers.Default) { - withContext(KernelService.kernelCoroutineContext()) { + withKernel { change { shared { register(XDebuggerValueLookupListeningStartedEntity) @@ -32,7 +33,7 @@ internal fun subscribeForDebuggingStart(cs: CoroutineScope, onStartListening: () internal fun subscribeForValueHintHideRequest(cs: CoroutineScope, onHintHidden: () -> Unit) { cs.launch(Dispatchers.Default) { - withContext(KernelService.kernelCoroutineContext()) { + withKernel { change { shared { register(XDebuggerValueLookupHideHintsRequestEntity) @@ -43,10 +44,12 @@ internal fun subscribeForValueHintHideRequest(cs: CoroutineScope, onHintHidden: onHintHidden() } // TODO: support multiple clients by clientId - cs.launch(Dispatchers.Default + KernelService.kernelCoroutineContext()) { - change { - shared { - entity.delete() + cs.launch(Dispatchers.Default) { + withKernel { + change { + shared { + entity.delete() + } } } } diff --git a/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupQuickEvaluateHandler.kt b/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupQuickEvaluateHandler.kt index 20e11d79805b..236ff50c41ae 100644 --- a/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupQuickEvaluateHandler.kt +++ b/platform/xdebugger-impl/frontend/src/com/intellij/xdebugger/impl/frontend/evaluate/quick/common/ValueLookupQuickEvaluateHandler.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.project.impl.asEntity import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.TextRange import com.intellij.platform.kernel.KernelService +import com.intellij.platform.kernel.withKernel import com.intellij.platform.rpc.RemoteApiProviderService import com.intellij.platform.util.coroutines.childScope import com.intellij.xdebugger.impl.evaluate.quick.common.AbstractValueHint @@ -51,20 +52,20 @@ open class ValueLookupManagerQuickEvaluateHandler : QuickEvaluateHandler() { val hintCoroutineScope = editor.childCoroutineScope("ValueLookupManagerValueHintParentScope") val hint: Deferred = coroutineScope.async(Dispatchers.IO) { - withContext(KernelService.kernelCoroutineContext()) { + withKernel { val remoteApi = RemoteApiProviderService.resolve(remoteApiDescriptor()) val projectEntity = project.asEntity() if (projectEntity == null) { - return@withContext null + return@withKernel null } val projectId = projectEntity.projectId val editorId = editor.editorId() val canShowHint = remoteApi.canShowHint(projectId, editorId, offset, type) if (!canShowHint) { - return@withContext null + return@withKernel null } val hint = ValueLookupManagerValueHint(hintCoroutineScope, project, projectId, editor, point, type, offset) - return@withContext hint + return@withKernel hint } } val hintPromise = hint.asCompletableFuture().asCancellablePromise() diff --git a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/ValueLookupManagerController.kt b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/ValueLookupManagerController.kt index cde25d167636..c11a05f5f682 100644 --- a/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/ValueLookupManagerController.kt +++ b/platform/xdebugger-impl/src/com/intellij/xdebugger/impl/evaluate/ValueLookupManagerController.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.platform.kernel.KernelService +import com.intellij.platform.kernel.withKernel import com.jetbrains.rhizomedb.EID import com.jetbrains.rhizomedb.Entity import fleet.kernel.DurableEntityType @@ -47,7 +48,7 @@ class ValueLookupManagerController(private val project: Project, private val cs: return } cs.launch(Dispatchers.Main) { - withContext(KernelService.kernelCoroutineContext()) { + withKernel { change { shared { register(XDebuggerValueLookupListeningStartedEntity) @@ -71,7 +72,7 @@ class ValueLookupManagerController(private val project: Project, private val cs: */ fun hideHint() { cs.launch(Dispatchers.Main) { - withContext(KernelService.kernelCoroutineContext()) { + withKernel { change { shared { register(XDebuggerValueLookupHideHintsRequestEntity)