mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[editor] IJPL-54 Use DocumentEntity in monolith mode
GitOrigin-RevId: e0e1d68a2d7b3473c35a2c411855191535d8c4bd
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3bf0fa1e38
commit
7aceab7bcd
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+172
-112
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user