diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorFactoryImpl.kt b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorFactoryImpl.kt index a66555a16ddf..988107ef6996 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorFactoryImpl.kt +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorFactoryImpl.kt @@ -21,6 +21,7 @@ import com.intellij.openapi.editor.event.EditorFactoryListener import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.highlighter.EditorHighlighter import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory +import com.intellij.openapi.editor.impl.ad.v2.AdTheManagerV2 import com.intellij.openapi.editor.impl.event.EditorEventMulticasterImpl import com.intellij.openapi.editor.impl.view.EditorPainter import com.intellij.openapi.extensions.ExtensionPointName @@ -208,6 +209,7 @@ class EditorFactoryImpl(coroutineScope: CoroutineScope?) : EditorFactory() { highlighter: EditorHighlighter?, afterCreation: ((EditorImpl) -> Unit)?, ): EditorImpl { + AdTheManagerV2.getInstance().bindLocalDocEntity(document) val editor = EditorImpl(document, isViewer, project, kind, file, highlighter) editor.putEditorId() // must be _before_ event firing diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index d5bd80a1f232..cb0543e3733b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -498,7 +498,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi }; } else { - myAdEditorModel = project == null ? null : com.intellij.openapi.editor.ex.AdTheManager.getInstance(project).createEditorModel(this); + myAdEditorModel = project == null ? null : AdTheManager.getInstance(project).createEditorModel(this); } myView = new EditorView(this, myEditorModel); @@ -1776,8 +1776,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi return myGutterComponent.isShowing(); } - @ApiStatus.Internal - public void repaintToScreenBottom(int startLine) { + private void repaintToScreenBottom(int startLine) { int yStartLine = logicalLineToY(startLine); repaintToScreenBottomStartingFrom(yStartLine); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/AdTheManagerImpl.kt b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/AdTheManagerImpl.kt index d1f2f84d2904..2b09ce5576de 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/AdTheManagerImpl.kt +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/AdTheManagerImpl.kt @@ -126,7 +126,7 @@ internal class AdTheManagerImpl(private val coroutineScope: CoroutineScope) : Ad withContext(Dispatchers.EDT) { if (!editor.isDisposed) { asOf(lastKnownDb) { - editor.repaintToScreenBottom(0) // TODO repaint partially + editor.component.repaint() } } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdDocumentSynchronizerV2.kt b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdDocumentSynchronizerV2.kt new file mode 100644 index 000000000000..499bfdb7f28a --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdDocumentSynchronizerV2.kt @@ -0,0 +1,62 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.editor.impl.ad.v2 + +import andel.operation.Operation +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.ex.PrioritizedDocumentListener +import com.intellij.openapi.editor.impl.ad.ThreadLocalRhizomeDB +import com.intellij.platform.pasta.common.DocumentEntity +import fleet.kernel.awaitCommitted +import fleet.kernel.change +import fleet.util.UID +import fleet.util.openmap.OpenMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import org.jetbrains.annotations.ApiStatus.Experimental + + +@Experimental +internal data class AdDocumentSynchronizerV2( + private val documentId: Any, // RdDocumentId + private val entityId: UID, + private val coroutineScope: CoroutineScope, + private val entityDeferred: Deferred, +): PrioritizedDocumentListener { + + override fun getPriority(): Int = Int.MIN_VALUE + 1 + + override fun documentChanged(event: DocumentEvent) { + val entityChange = coroutineScope.async { + val operation = operation(event) + val entity = entityDeferred.await() + change { // it is a shared change + entity.mutate(this, OpenMap.empty()) { + edit(operation) + } + } + awaitCommitted() + } + // TODO: pumping events in runWithModalProgressBlocking ruins the models + //@Suppress("HardCodedStringLiteral") + //runWithModalProgressBlocking( + // ModalTaskOwner.guess(), + // "Shared document entity synchronization $entityId", + //) { entityChange.await() } + runBlocking { entityChange.await() } + ThreadLocalRhizomeDB.setThreadLocalDb(ThreadLocalRhizomeDB.lastKnownDb()) + } + + private fun operation(event: DocumentEvent): Operation { + val oldFragment = event.oldFragment.toString() + val newFragment = event.newFragment.toString() + val lengthBefore = event.document.textLength - newFragment.length + oldFragment.length + return Operation.Companion.replaceAt( + offset = event.offset.toLong(), + oldText = oldFragment, + newText = newFragment, + totalLength = lengthBefore.toLong(), + ) + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdTheManagerV2.kt b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdTheManagerV2.kt index bc8861ab4e8a..b2adba029ad2 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdTheManagerV2.kt +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/v2/AdTheManagerV2.kt @@ -2,7 +2,6 @@ package com.intellij.openapi.editor.impl.ad.v2 import andel.intervals.AnchorStorage -import andel.operation.Operation import andel.text.Text import com.intellij.openapi.Disposable import com.intellij.openapi.application.EDT @@ -11,27 +10,27 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document -import com.intellij.openapi.editor.event.DocumentEvent -import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.ex.DocumentEx -import com.intellij.openapi.editor.ex.PrioritizedDocumentListener import com.intellij.openapi.editor.ex.util.EditorUtil -import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.editor.impl.ad.ThreadLocalRhizomeDB +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.platform.pasta.common.DocumentEntity import com.intellij.platform.pasta.common.DocumentEntity.Companion.EditLogAttr import com.intellij.platform.pasta.common.DocumentEntity.Companion.SharedAnchorStorageAttr import com.intellij.platform.pasta.common.DocumentEntity.Companion.TextAttr import com.intellij.platform.pasta.common.DocumentEntity.Companion.WritableAttr import com.intellij.platform.pasta.common.createEmptyEditLog -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.util.registry.Registry -import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.util.coroutines.childScope -import com.intellij.util.asDisposable +import com.intellij.util.awaitCancellationAndInvoke import com.intellij.util.concurrency.AppExecutorUtil +import com.intellij.util.concurrency.ThreadingAssertions +import com.intellij.util.ui.EDT import fleet.kernel.Durable -import fleet.kernel.awaitCommitted import fleet.kernel.change import fleet.kernel.rete.each import fleet.kernel.rete.filter @@ -39,7 +38,6 @@ import fleet.kernel.rete.first import fleet.kernel.shared import fleet.kernel.transactor import fleet.util.UID -import fleet.util.openmap.OpenMap import kotlinx.coroutines.* import org.jetbrains.annotations.ApiStatus.Experimental import java.util.* @@ -58,44 +56,15 @@ class AdTheManagerV2(private val coroutineScope: CoroutineScope) { fun getInstance(): AdTheManagerV2 = service() } - private val docToEntityMap = IdentityHashMap() - private val docToSynchronizerMap = IdentityHashMap() + private val docToHandle = IdentityHashMap() fun bindBackendDocEntity(file: VirtualFile, lazyDocumentId: (document: DocumentEx) -> Any) { if (isEnabled()) { val document = FileDocumentManager.getInstance().getDocument(file) if (document is DocumentEx) { - synchronized(docToEntityMap) { - val entityHandle = docToEntityMap[document] - val nextEntityHandle = if (entityHandle != null) { - entityHandle.inc() - } else { - val documentId = lazyDocumentId(document) - val uid = hackyDocumentId(documentId) - val entityRef = AtomicReference() - val entityDeferred = coroutineScope.async(AD_DISPATCHER) { - val entity = change { - shared { - DocumentEntity.new { - it[Durable.Id] = uid - it[TextAttr] = Text.fromString(document.immutableCharSequence.toString()) - it[WritableAttr] = true - it[EditLogAttr] = createEmptyEditLog() - it[SharedAnchorStorageAttr] = AnchorStorage.empty() - } - } - } - entityRef.set(entity) - entity - } - val createdEntityHandle = DocumentEntityHandle(documentId, uid, entityDeferred, entityRef, refCount = 1) - val synchronizer = DocumentToEntitySynchronizer(createdEntityHandle, coroutineScope) - val existing = docToSynchronizerMap.put(document, synchronizer) - assert(existing == null) { "doc synchronizer must be absent" } - document.addDocumentListener(synchronizer, coroutineScope.asDisposable()) - createdEntityHandle - } - docToEntityMap[document] = nextEntityHandle + synchronized(docToHandle) { + removeLocalDocEntity(document) + bindBackendDocEntity(document, BindType.BACKEND, lazyDocumentId) } } } @@ -103,49 +72,51 @@ class AdTheManagerV2(private val coroutineScope: CoroutineScope) { fun bindFrontendDocEntity(documentId: Any, document: Document?) { if (isEnabled() && document is DocumentEx) { - val uid = hackyDocumentId(documentId) - synchronized(docToEntityMap) { - val entityRef = AtomicReference() - val entityDeferred: Deferred = coroutineScope.async(AD_DISPATCHER) { - val entity = DocumentEntity.each().filter { it.uid == uid }.first() - entityRef.set(entity) - entity + synchronized(docToHandle) { + removeLocalDocEntity(document) + bindDocEntity( + document, + BindType.FRONTEND, + { documentId }, + createEntity = { entityId -> DocumentEntity.each().filter { it.uid == entityId }.first() }, + deleteEntity = {}, + ) + } + } + } + + fun bindLocalDocEntity(document: Document) { + if (isEnabled() && document is DocumentEx) { + synchronized(docToHandle) { + val handle = docToHandle[document] + if (handle == null || handle.isLocal()) { + bindBackendDocEntity(document, BindType.LOCAL) { UUID.randomUUID() } } - docToEntityMap[document] = DocumentEntityHandle(documentId, uid, entityDeferred, entityRef, refCount = Int.MAX_VALUE) } } } fun releaseDocEntity(document: DocumentEx) { if (isEnabled()) { - synchronized(docToEntityMap) { - val entityHandle = docToEntityMap[document] - checkNotNull(entityHandle) { "doc entity not found" } - val nextEntityHandle = entityHandle.dec() - if (nextEntityHandle == null) { - val synchronizer = docToSynchronizerMap.remove(document) - if (synchronizer != null) { // backend branch - document.removeDocumentListener(synchronizer) - coroutineScope.launch(AD_DISPATCHER) { - val entity = entityHandle.entity() - change { - shared { - entity.delete() - } - } - } - } + synchronized(docToHandle) { + val handle = docToHandle[document] + checkNotNull(handle) { "doc entity not found" } + val nextHandle = handle.decRef() + if (nextHandle != null) { + docToHandle[document] = nextHandle + } else { + handle.dispose() + docToHandle.remove(document) } - docToEntityMap[document] = nextEntityHandle } } } fun getAdDocument(document: DocumentEx): DocumentEx? { if (isEnabled()) { - val entity = synchronized(docToEntityMap) { - docToEntityMap[document] - }?.entity0() + val entity = synchronized(docToHandle) { + docToHandle[document] + }?.entity() if (entity != null) { ThreadLocalRhizomeDB.setThreadLocalDb(ThreadLocalRhizomeDB.lastKnownDb()) return AdDocumentV2(entity) @@ -154,23 +125,115 @@ class AdTheManagerV2(private val coroutineScope: CoroutineScope) { return null } - fun bindEditor(editor: EditorImpl) { + fun bindEditor(editor: Editor) { if (isEnabled()) { val cs = coroutineScope.childScope("editor repaint on doc entity change") val disposable = Disposable { cs.cancel() } EditorUtil.disposeWithEditor(editor, disposable) cs.launch(AD_DISPATCHER) { - transactor().log.collect { - // TODO: track only text changes + transactor().log.collect { // TODO: track only text changes withContext(Dispatchers.EDT) { - editor.repaintToScreenBottom(0) + if (!editor.isDisposed) { + editor.component.repaint() // TODO: repaint partially + } } } } } } - private fun hackyDocumentId(documentId: Any): UID { + private fun bindBackendDocEntity( + document: DocumentEx, + bindType: BindType, + lazyDocumentId: (document: DocumentEx) -> Any, + ) { + assert(isEnabled()) + assert(bindType != BindType.FRONTEND) + val text = document.immutableCharSequence + bindDocEntity( + document, + bindType, + lazyDocumentId, + createEntity = { entityId -> + change { + shared { + DocumentEntity.new { + it[Durable.Id] = entityId + it[TextAttr] = Text.fromString(text.toString()) + it[WritableAttr] = true + it[EditLogAttr] = createEmptyEditLog() + it[SharedAnchorStorageAttr] = AnchorStorage.empty() + } + } + } + }, + deleteEntity = { entity -> + change { + shared { + entity.delete() + } + } + }, + ) + } + + private fun bindDocEntity( + document: DocumentEx, + bindType: BindType, + lazyDocumentId: (document: DocumentEx) -> Any, + createEntity: suspend (UID) -> DocumentEntity, + deleteEntity: suspend (DocumentEntity) -> Unit, + ) { + assert(isEnabled()) + if (!EDT.isCurrentThreadEdt()) { + ThreadingAssertions.assertReadAccess() + } + synchronized(docToHandle) { + val handle = docToHandle[document] + docToHandle[document] = if (handle != null) { + handle.incRef() + } else { + val documentId = lazyDocumentId(document) + val entityId = hackyEntityId(documentId) + val cs = coroutineScope.childScope("docEntityScope($entityId)", AD_DISPATCHER) + val entityRef = AtomicReference() + val entityDeferred = cs.async { + val entity = createEntity(entityId) + entityRef.set(entity) + entity + } + if (bindType != BindType.FRONTEND) { + val documentListener = AdDocumentSynchronizerV2(documentId, entityId, cs, entityDeferred) + document.addDocumentListener(documentListener) + @Suppress("OPT_IN_USAGE") + cs.awaitCancellationAndInvoke { + document.removeDocumentListener(documentListener) + val entity = entityDeferred.await() + deleteEntity(entity) + } + } + DocumentEntityHandle( + documentId, + entityId, + bindType, + 1, + cs, + entityDeferred, + entityRef, + ) + } + } + } + + private fun removeLocalDocEntity(document: DocumentEx) { + val handle = docToHandle[document] + if (handle != null && handle.isLocal()) { + handle.dispose() + docToHandle.remove(document) + } + } + + private fun hackyEntityId(documentId: Any): UID { val bytes = documentId.toString().toByteArray() val uuid = UUID.nameUUIDFromBytes(bytes) return UID.fromString(uuid.toString()) @@ -181,50 +244,47 @@ class AdTheManagerV2(private val coroutineScope: CoroutineScope) { } } +private enum class BindType { + LOCAL, FRONTEND, BACKEND +} + private data class DocumentEntityHandle( - private val documentId: Any, + private val documentId: Any, // RdDocumentId private val entityId: UID, + private val bindType: BindType, + private val refCount: Int, + private val coroutineScope: CoroutineScope, private val entityDeferred: Deferred, private val entityRef: AtomicReference, - private val refCount: Int, ) { - fun inc(): DocumentEntityHandle = copy(refCount = refCount + 1) - fun dec(): DocumentEntityHandle? = if (refCount > 1) copy(refCount = refCount - 1) else null - suspend fun entity(): DocumentEntity = entityDeferred.await() - fun entity0(): DocumentEntity = entityRef.get() ?: runBlocking { entity() } -} -private class DocumentToEntitySynchronizer( - private val entityHandle: DocumentEntityHandle, - private val coroutineScope: CoroutineScope, -) : PrioritizedDocumentListener { + fun incRef(): DocumentEntityHandle { + assert(refCount > 0) + return copy(refCount = refCount + 1) + } - override fun getPriority(): Int = Int.MIN_VALUE + 1 + fun decRef(): DocumentEntityHandle? { + assert(refCount > 0) + return if (refCount > 1) copy(refCount = refCount - 1) else null + } - override fun documentChanged(event: DocumentEvent) { - val operation = operation(event) - val deferred = coroutineScope.async(AD_DISPATCHER) { - val entity = entityHandle.entity() - change { // it is a shared change - entity.mutate(this, OpenMap.empty()) { - edit(operation) - } - } - awaitCommitted() + fun entity(): DocumentEntity { + val entity = entityRef.get() + if (entity != null) { + return entity } - runBlocking { deferred.await() } - ThreadLocalRhizomeDB.setThreadLocalDb(ThreadLocalRhizomeDB.lastKnownDb()) + @Suppress("HardCodedStringLiteral") + return runWithModalProgressBlocking( + ModalTaskOwner.guess(), + "Shared document entity creation $entityId", + ) { entityDeferred.await() } } - private fun operation(event: DocumentEvent): Operation { - val oldText = event.oldFragment.toString() - val newText = event.newFragment.toString() - val lengthBefore = event.document.textLength - newText.length + oldText.length - return Operation.Companion.replaceAt( - offset = event.offset.toLong(), - oldText = oldText, - newText = newText, - totalLength = lengthBefore.toLong(), - ) + fun isLocal(): Boolean { + return bindType == BindType.LOCAL + } + + fun dispose() { + coroutineScope.cancel() } }