From 1fba937893acc6c1ca99e24a3b28246f73ab6aa4 Mon Sep 17 00:00:00 2001 From: Alexander Trushev Date: Wed, 4 Feb 2026 17:29:40 +0100 Subject: [PATCH] [editor] IJPL-54 Sync entity->documentImpl GitOrigin-RevId: f43b36fcbab6b9b65dccd403e53c5264ffb71431 --- .../pasta/src/common/DbMutableDocument.kt | 10 +- .../kernel/pasta/src/common/DocumentChange.kt | 3 + .../src/common/SharedDocumentInstructions.kt | 1 + .../ad/document/AdDocumentSynchronizer.kt | 115 +++++++++++++----- 4 files changed, 97 insertions(+), 32 deletions(-) diff --git a/platform/kernel/pasta/src/common/DbMutableDocument.kt b/platform/kernel/pasta/src/common/DbMutableDocument.kt index 6e4b6081179d..6939cf69ac27 100644 --- a/platform/kernel/pasta/src/common/DbMutableDocument.kt +++ b/platform/kernel/pasta/src/common/DbMutableDocument.kt @@ -6,6 +6,7 @@ import andel.editor.AnchorLifetime import andel.editor.DocumentComponent import andel.editor.DocumentComponentKey import andel.editor.DocumentMeta +import andel.editor.DocumentMetaKey import andel.editor.MutableDocument import andel.editor.RangeMarkerId import andel.intervals.AnchorStorage @@ -28,6 +29,11 @@ import fleet.util.openmap.MutableBoundedOpenMap import fleet.util.openmap.MutableOpenMap +object DocToEntityUpdate : DocumentMetaKey + +internal val DbMutableDocument.docToDb: Boolean? + get() = meta[DocToEntityUpdate] + internal class DbMutableDocument( val dbDocument: DocumentEntity, private val changeScope: ChangeScope, @@ -79,7 +85,9 @@ internal class DbMutableDocument( ChangeDocument( operationId = UID.random(), documentId = dbDocument.eid, - operation = versionedOperation.rebase(this@DbMutableDocument)), + operation = versionedOperation.rebase(this@DbMutableDocument), + docToDb = docToDb ?: false, + ), ) } val textAfter = text diff --git a/platform/kernel/pasta/src/common/DocumentChange.kt b/platform/kernel/pasta/src/common/DocumentChange.kt index e99df8462f69..2ee1df2b8e7a 100644 --- a/platform/kernel/pasta/src/common/DocumentChange.kt +++ b/platform/kernel/pasta/src/common/DocumentChange.kt @@ -27,6 +27,7 @@ data class ChangeDocument( val documentId: EID, val operationId: UID, val operation: Operation, + val docToDb: Boolean, override val seed: Long = Random.nextLong(), ) : Instruction { @@ -49,6 +50,7 @@ data class ChangeDocument( documentId = documentUID, operationId = instruction.operationId, operation = instruction.operation, + docToDb = instruction.docToDb, seed = instruction.seed, ) ) @@ -65,6 +67,7 @@ data class ChangeDocument( documentId = documentEID, operationId = sharedInstruction.operationId, operation = sharedInstruction.operation, + docToDb = sharedInstruction.docToDb, seed = sharedInstruction.seed, )) } diff --git a/platform/kernel/pasta/src/common/SharedDocumentInstructions.kt b/platform/kernel/pasta/src/common/SharedDocumentInstructions.kt index f830a3fe4010..4fb7123b1a11 100644 --- a/platform/kernel/pasta/src/common/SharedDocumentInstructions.kt +++ b/platform/kernel/pasta/src/common/SharedDocumentInstructions.kt @@ -12,6 +12,7 @@ data class SharedChangeDocument( val documentId: UID, val operationId: UID, val operation: Operation, + val docToDb: Boolean, val seed: Long, ) diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/document/AdDocumentSynchronizer.kt b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/document/AdDocumentSynchronizer.kt index 9e90135767d0..4e3aebc44e1f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/document/AdDocumentSynchronizer.kt +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ad/document/AdDocumentSynchronizer.kt @@ -1,8 +1,13 @@ // Copyright 2000-2026 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.document +import andel.operation.Op import andel.operation.Operation +import andel.operation.compose +import andel.operation.isNotIdentity import com.intellij.openapi.Disposable +import com.intellij.openapi.application.edtWriteAction +import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level import com.intellij.openapi.components.service @@ -11,17 +16,27 @@ import com.intellij.openapi.editor.ex.DocumentEx import com.intellij.openapi.editor.ex.PrioritizedDocumentListener import com.intellij.openapi.editor.impl.ad.AdTheManager import com.intellij.openapi.editor.impl.ad.util.ThreadLocalRhizomeDB +import com.intellij.platform.pasta.common.ChangeDocument +import com.intellij.platform.pasta.common.DocToEntityUpdate import com.intellij.platform.pasta.common.DocumentEntity import com.intellij.platform.util.coroutines.childScope import com.intellij.util.ui.EDT +import com.jetbrains.rhizomedb.asOf +import fleet.kernel.SubscriptionEvent +import fleet.kernel.byUidOrNull import fleet.kernel.change +import fleet.kernel.rebase.TransactionResult +import fleet.kernel.rebase.TransactionResultKey import fleet.kernel.rebase.awaitCommitted -import fleet.kernel.rebase.shared +import fleet.kernel.transactor import fleet.util.openmap.OpenMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.lang.ref.Reference +import java.lang.ref.WeakReference @Service(Level.APP) @@ -42,6 +57,7 @@ class AdDocumentSynchronizer(private val coroutineScope: CoroutineScope): Dispos DocToEntitySynchronizer( debugName, entity, + WeakReference(document), cs, ) ) @@ -52,51 +68,57 @@ class AdDocumentSynchronizer(private val coroutineScope: CoroutineScope): Dispos private class DocToEntitySynchronizer( private val debugName: String, private val entity: DocumentEntity, - //private val document: DocumentEx, + private val documentRef: Reference, private val coroutineScope: CoroutineScope ) : PrioritizedDocumentListener { - //private var documentChanging = false - - //init { - // coroutineScope.launch { - // var initial = true - // entity.asQuery()[DocumentEntity.TextAttr].collect { text -> - // // TODO do we need to process first change in some cases (e.g. on the frontend)? - // if (initial) { - // initial = false - // return@collect - // } - // - // writeAction { - // documentChanging = true - // try { - // document.setText(text.view().charSequence()) - // } - // finally { - // documentChanging = false - // } - // } - // } - // } - //} + init { + coroutineScope.launch { + transactor().log.collect { e -> + if (documentRef.get() == null) { + cancel() + return@collect + } + if (e is SubscriptionEvent.Next) { + val change = e.change + val tx = change.meta[TransactionResultKey] + if (tx is TransactionResult.TransactionApplied) { + val op = tx.tx.instructions.asSequence() + .filter { it.name == ChangeDocument.instructionName } + .map { it.instruction.get(ChangeDocument.serializer) } + .filter { + asOf(change.dbAfter) { + val changedEntity = byUidOrNull(it.documentId) + changedEntity?.eid == entity.eid + } + } + .filter { !it.docToDb } + .map { it.operation } + .toList() + .compose() + if (op.isNotIdentity()) { + entityToDocumentImplChange(op) + } + } + } + } + } + } override fun getPriority(): Int = Int.MIN_VALUE + 1 override fun documentChanged(event: DocumentEvent) { - //if (documentChanging) return - val entityChange = coroutineScope.async { val operation = operation(event) change { // shared should not be used here, otherwise an exception is going to be thrown during rebase // `mutate` should decide when to use `shared` // TODO check that markups work // shared to mutate shared document components (e.g., AdMarkupModel) - shared { // shared to mutate shared document components (e.g., AdMarkupModel) - entity.mutate(this, OpenMap.empty()) { + //shared { // shared to mutate shared document components (e.g., AdMarkupModel) + entity.mutate(this, OpenMap { set(DocToEntityUpdate, true) }) { edit(operation) } - } + //} } //TODO we should not wait for awaitCommitted() @@ -108,6 +130,37 @@ class AdDocumentSynchronizer(private val coroutineScope: CoroutineScope): Dispos } } + private suspend fun entityToDocumentImplChange(op: Operation) { + edtWriteAction { + CommandProcessor.getInstance().executeCommand( + null, + Runnable { + val document = documentRef.get() + if (document != null) { + var offset = 0 + for (item in op.ops) { + when (item) { + is Op.Replace -> { + document.replaceString( + offset, + offset + item.delete.length, + item.insert, + ) + offset += item.insert.length + } + is Op.Retain -> { + offset += item.len.toInt() + } + } + } + } + }, + null, + null, + ) + } + } + private fun operation(event: DocumentEvent): Operation { val oldFragment = event.oldFragment.toString() val newFragment = event.newFragment.toString()