[editor] IJPL-54 Use DocumentEntity in monolith mode

GitOrigin-RevId: e0e1d68a2d7b3473c35a2c411855191535d8c4bd
This commit is contained in:
Alexandr Trushev
2025-02-21 22:43:10 +00:00
committed by intellij-monorepo-bot
parent 3bf0fa1e38
commit 7aceab7bcd
5 changed files with 239 additions and 116 deletions
@@ -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
@@ -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);
}
@@ -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()
}
}
}
@@ -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<DocumentEntity>,
): 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(),
)
}
}
@@ -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<DocumentEx, DocumentEntityHandle>()
private val docToSynchronizerMap = IdentityHashMap<DocumentEx, DocumentListener>()
private val docToHandle = IdentityHashMap<DocumentEx, DocumentEntityHandle>()
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<DocumentEntity>()
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<DocumentEntity>()
val entityDeferred: Deferred<DocumentEntity> = 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<DocumentEntity>()
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<DocumentEntity>,
private val entityRef: AtomicReference<DocumentEntity>,
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()
}
}